一、如何优化DOM树解析过程?
(一) 根本原则:理解关键渲染路径(CRP)
优化 DOM 解析的核心在于缩短关键渲染路径。即最小化、优化或避免阻塞 DOM 和 CSSOM 构建的因素。
- DOM(文档对象模型):HTML 被解析后生成的树状结构。
- CSSOM(CSS对象模型):CSS 被解析后生成的树状结构。
- 渲染树(Render Tree):DOM 和 CSSOM 结合后生成,用于计算布局和绘制。
阻塞关系:
- HTML 解析 -> 构建 DOM:默认过程。
<script>会阻塞 DOM 构建:因为 JS 可能会修改 DOM 结构,所以浏览器会停止 HTML 解析,先下载(如果是外部脚本)并执行 JS,然后再继续。- CSS 会阻塞 JS 的执行:因为 JS 可能会请求 CSS 信息,所以浏览器会确保所有之前的 CSS(特别是
<script>标签之前的CSS)都已被下载并解析为 CSSOM 后,才执行 JS。 - CSS 不会阻塞 DOM 解析,但会阻塞渲染:浏览器会等 CSSOM 构建完成后才进行渲染(避免“无样式内容闪烁”)。
(二) HTML 层面的优化(减少DOM的“量”和“复杂度”)
这是最直接优化解析过程的方法。解析一个更小、更简单的树总是更快。
- 最小化 DOM 深度和节点数量:
-
- 原因:更少的节点意味着更少的内存占用、更快的样式计算、更快的布局重排(Reflow)。
- 做法:
-
-
- 避免不必要的包装
<div>或<span>。现代 CSS(如 Flexbox、Grid)可以减少用于布局的冗余标签。 - 使用语义化标签(如
<article>,<section>,<nav>)而不是一堆<div>,它们在结构上更清晰。 - 定期审查代码,移除僵尸节点或注释掉的代码块。
- 避免不必要的包装
-
- 移除空白和注释:
-
- 原因:文本节点(包括空白符)也是 DOM 节点。大量的空白和注释会增加不必要的节点数量。
- 做法:在生产环境中使用构建工具(如 Webpack, Gulp, Vite)对 HTML 进行压缩(Minify),移除所有不必要的字符。
- 使用 HTML 惰性加载属性:
loading="lazy":对 <img> 和 <iframe> 使用此属性。它不会加快初始 DOM 解析,但会推迟这些非关键资源的加载,减少主线程的初始压力,让浏览器更专注于解析和渲染核心内容。
<img src="image.jpg" loading="lazy" alt="...">
<iframe src="content.html" loading="lazy"></iframe>
(三) CSS 层面的优化(减少对CSSOM的阻塞)
- 精简和压缩 CSS:
-
- 使用工具(如 cssnano、PurgeCSS)移除未使用的 CSS 规则和空白字符。更少的 CSS 规则意味着更快的 CSSOM 构建。
- 避免使用
@import:
-
- 原因:
@import会在 CSS 文件中发起一个新的 HTTP 请求,并且是同步的。它会阻止浏览器并行下载其他资源,直到该@import的资源被下载和解析。 - 做法:始终使用
<link>标签在 HTML 中引入 CSS,这样可以并行下载。
- 原因:
- 将 CSS 放在头部(
<head>):
-
- 原因:让浏览器尽早发现 CSS 并开始构建 CSSOM。由于 CSS 不会阻塞 DOM 解析,但会阻塞渲染,尽早加载可以保证浏览器在有 DOM 和 CSSOM 后能立即渲染,减少白屏时间。
- 使用媒体查询(Media Queries):
-
- 对非立即需要的 CSS(如打印样式、特定屏幕尺寸的样式)使用
media属性。浏览器会以低优先级下载它们,避免它们阻塞关键渲染路径。
- 对非立即需要的 CSS(如打印样式、特定屏幕尺寸的样式)使用
<!-- 关键CSS,阻塞渲染 -->
<link rel="stylesheet" href="styles.css">
<!-- 非关键CSS,不阻塞渲染 -->
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="large-screen.css" media="(min-width: 1200px)">
(四) JavaScript 层面的优化(减少对DOM构建的阻塞)
这是优化 DOM 解析的重中之重,因为 JS 是主要的阻塞源。
- 将 JavaScript 放在底部(
</body>之前):
-
- 经典方法。将所有的
<script>标签放在页面内容之后。这样 DOM 的解析基本完成,不会被 JS 所阻塞。
- 经典方法。将所有的
- 使用
async和defer属性:
-
async(异步):
-
-
- 脚本的下载不会阻塞 HTML 解析。
- 脚本下载完成后会立即执行,此时会阻塞 HTML 解析。
- 适用于独立且不依赖DOM或其他脚本的第三方脚本(如 analytics)。
-
-
defer(延迟):
-
-
- 脚本的下载不会阻塞 HTML 解析。
- 脚本会等到 HTML 解析完全完成后(
DOMContentLoaded事件之前),按顺序执行。 - 适用于需要操作DOM但又不急的脚本,或者有依赖关系的脚本。
-
<!-- 可能会在中间执行,阻塞解析 -->
<script async src="script.js"></script>
<!-- 保证在最后执行,不阻塞解析 -->
<script defer src="script.js"></script>
- 避免使用
document.write():
-
- 在现代浏览器中,尤其是在异步或延迟的脚本中,
document.write()会破坏 DOM 结构,并可能导致浏览器执行完整的页面重解析,性能极差。
- 在现代浏览器中,尤其是在异步或延迟的脚本中,
- 使用
requestAnimationFrame或setTimeout拆分长任务:
-
- 如果必须执行大量的 DOM 操作,不要一次性做完。这会造成主线程长时间被占用,导致页面卡顿。可以将任务拆分成小块,在浏览器的空闲时间执行。
- 使用虚拟DOM(Virtual DOM)库:
-
- 像 React、Vue 这样的框架使用 Virtual DOM。它们先在内存中的 JavaScript 对象(虚拟DOM)上进行更改,然后通过高效的 Diff 算法计算出最小化的变更集,最后再一次性应用到真实 DOM 上。这大大减少了直接操作真实 DOM 的次数,而 DOM 操作是昂贵的。
(五) 其他高级优化
- 预加载和预连接:
-
- 使用
<link rel="preload">告诉浏览器以高优先级下载关键资源(如关键CSS、Web字体)。 - 使用
<link rel="preconnect">或<link rel="dns-prefetch">提前与第三方源建立连接,减少 DNS 查询和 TCP 握手时间。
- 使用
<link rel="preload" href="critical.css" as="style">
<link rel="preconnect" href="https://fonts.gstatic.com">
- 服务器端渲染(SSR):
-
- 原理:在服务器上生成页面的初始 HTML。用户收到时已经有一个完整的 DOM 结构,JavaScript 再随后激活(Hydrate)交互功能。
- 优势:极大改善首屏加载时间和可交互时间,因为浏览器无需等待所有 JS 下载和执行完就能显示内容。
二、DNS预解析是什么?怎么实现?
(一) 什么是 DNS 预解析?
DNS 预解析(DNS Prefetching) 是一种前端性能优化手段,它允许浏览器在后台提前执行第三方域名的 DNS 解析,从而减少后续实际请求资源时的延迟。
(二) 为什么需要它?—— 解决什么问题
要理解它的价值,我们需要先看一个网络请求的生命周期。当浏览器需要从另一个域名(例如,从 https://www.example.com 去请求 https://cdn.example-network.com/image.jpg)获取资源时,大致需要以下步骤:
- DNS 解析:浏览器需要找出
cdn.example-network.com这个域名对应的真实服务器 IP 地址。这个过程就是 DNS 查询。 - TCP 握手:浏览器拿到 IP 后,会与服务器建立 TCP 连接(通常是三次握手)。
- TLS 协商(如果是 HTTPS):如果使用 HTTPS,还需要进行 TLS 握手以建立安全连接。
- 发送请求 & 接收响应:连接建立后,浏览器才真正发送 HTTP 请求并等待服务器返回资源。
DNS 解析是第一步,但它是一个潜在的瓶颈:
- 它通常需要花费 20-120 ms。
- 它必须在一个域名的第一次请求时发生。
- 如果页面中存在多个来自同一新域名的资源,每个资源都需要等待 DNS 解析完成,就会造成排队和延迟(虽然浏览器有缓存,但第一次无法避免)。
DNS 预解析的作用就是提前完成第一步。它告诉浏览器:“我稍后会需要从这个域名加载资源,你现在有空的时候先帮我把 DNS 解析了吧。” 这样,当浏览器真正需要请求该域名的资源时,DNS 解析这一步已经完成,可以直接建立连接,节省了宝贵的时间,显著提升了页面加载性能,特别是对于使用了大量第三方资源(字体、分析脚本、广告、CDN 资源等)的网站。
(三) 如何实现 DNS 预解析?
实现 DNS 预解析非常简单,主要通过在你的 HTML 文档的 <head> 部分添加特定的 <link> 标签来实现。
1. 手动添加 Link 标签(最常用)
这是最直接、兼容性最好的方法。
语法:
<link rel="dns-prefetch" href="https://需要预解析的域名">
rel="dns-prefetch":明确指示浏览器这是一个 DNS 预解析指令。href:指定需要预解析的域名。注意:这里只需要指定协议和域名,不需要指定具体的路径。协议(https:)最好写上,但即使只写//domain.com也可以。
示例:
假设你的页面将要从以下第三方服务加载资源:
- Google Fonts(字体):
https://fonts.googleapis.com - Google Analytics(分析):
https://www.google-analytics.com - 你自己的 CDN:
https://cdn.yourdomain.com
你可以在 HTML 的 <head> 中添加:
<head>
...
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<link rel="dns-prefetch" href="https://cdn.yourdomain.com">
...
</head>
2. 通过 HTTP 响应头实现
除了在 HTML 中写入,服务器还可以通过在 HTTP 响应头中返回 Link 字段来指示浏览器进行预解析。这对于动态页面或者无法直接修改 HTML 模板的情况非常有用。
语法(在服务器的响应配置中设置):
Link: <https://fonts.googleapis.com>; rel=dns-prefetch
示例(在 Nginx 配置文件中):
server {
listen 80;
server_name yourdomain.com;
location / {
...
add_header Link "<https://fonts.googleapis.com>; rel=dns-prefetch";
...
}
}
(注意:添加多个域名需要多条 add_header 指令,但需注意 Nginx 中 add_header 的继承规则)
3. 浏览器自动解析
现代浏览器(如 Chrome、Firefox)已经非常智能。它们会解析在 HTML 中遇到的各类资源链接(如 <a>, <img>, <link>, <script>, <style> 标签的 href 或 src 属性),并自动为这些域名进行 DNS 预解析,甚至不需要开发者手动添加 dns-prefetch 指令。
但是,手动添加仍然在以下情况下至关重要:
- 非标准或隐藏的资源:通过 JavaScript 动态加载的资源、CSS 中的
@font-face规则里引用的字体文件、通过url()引用的背景图片等。浏览器在初始解析 HTML 时无法发现这些资源,因此不会自动预解析它们的域名。 - 重定向域名:如果你知道一个 URL 最终会重定向到另一个域名,可以预解析最终的目标域名。
- 确保高优先级:手动添加可以确保浏览器更早地注意到这些域名并优先处理。
(四) 最佳实践和注意事项
- 仅对跨域域名使用:对于你网站自身的域名,DNS 解析通常已经在初始请求页面时完成了,无需再预解析。应专注于第三方域名。
- 不要过度使用:每个预解析请求虽然消耗极小的带宽和 CPU,但如果一次性预解析几十个域名,可能会与关键资源争夺网络带宽和 CPU 资源。只预解析最关键、最可能用到的第三方域名。
- 与 Preconnect 结合使用:
dns-prefetch只完成了 DNS 解析。还有一个更“强大”的兄弟指令叫preconnect。
preconnect 不仅会提前进行 DNS 解析,还会提前进行 TCP 握手和 TLS 协商。它建立了完整的连接,成本更高,但收益也更大。
建议:对极其关键的核心第三方源使用 preconnect,对其他的使用 dns-prefetch
<!-- 建立完整连接,用于最关键的资源 -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<!-- 仅DNS解析,用于其他重要资源 -->
<link rel="dns-prefetch" href="https://www.google-analytics.com">
(注意:使用 preconnect 时,通常建议加上 crossorigin 属性)
- 兼容性:
dns-prefetch得到了所有主流浏览器的广泛支持,可以放心使用。preconnect的兼容性也非常好,但略新于dns-prefetch。
三、如何防止CSS阻塞渲染?
(一) 核心结论先行
CSS 默认会阻塞渲染。目标是只让关键CSS阻塞渲染,而对非关键CSS采用异步加载策略,从而让页面内容尽快呈现给用户。
(二) 为什么CSS会阻塞渲染?
浏览器渲染页面的“关键渲染路径”如下:
- 构建 DOM:解析 HTML 生成 DOM 树。
- 构建 CSSOM:解析 CSS 生成 CSSOM 树。
- 合并:将 DOM 和 CSSOM 合并成渲染树(Render Tree)。
- 布局:计算每个节点的位置和大小(Layout)。
- 绘制:将像素绘制到屏幕上(Paint)。
浏览器之所以会阻塞渲染,是因为它要避免 FOUC(Flash of Unstyled Content),即“无样式内容闪烁”。如果浏览器在 CSS 加载完之前就显示了已解析的 HTML,用户会先看到丑陋的无样式页面,然后突然样式被应用,页面发生跳动。这是一种很差的用户体验。
因此,浏览器的规则是:CSSOM 构建完成之前,浏览器不会渲染页面。
(三) 如何防止CSS阻塞渲染?(实战策略)
以下方法按推荐度和实用性排序。
1. 策略一:使用 media 属性进行条件加载(最简单、最有效)
这是最符合Web标准且高效的方法。你可以通过 media 属性告诉浏览器某些CSS资源只在特定条件下使用,浏览器会据此调整加载优先级。
- 核心思想:将CSS分为关键CSS和非关键CSS。
- 关键CSS:用于首屏、Above-the-fold(折叠上方)内容的样式。必须同步加载,阻塞渲染。
- 非关键CSS:打印样式、特定屏幕尺寸的样式、折叠下方的样式等。可以异步加载。
实现方法:
在 <link> 标签上使用 media 属性。
<!-- 关键CSS:始终阻塞渲染 -->
<link rel="stylesheet" href="core.css">
<!-- 非关键CSS:指定媒体查询 -->
<link rel="stylesheet" href="print.css" media="print"> <!-- 仅用于打印,不阻塞渲染 -->
<link rel="stylesheet" href="large-screen.css" media="(min-width: 1200px)"> <!-- 大屏才用,不阻塞渲染 -->
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)"> <!-- 小屏才用,不阻塞渲染 -->
浏览器行为:
- 浏览器会下载所有CSS文件(优先级不同)。
- 但对于标记了
media的CSS,浏览器会以最低优先级(Lowest Priority)异步下载它们,不会阻塞渲染和DOMContentLoaded事件。 - 下载完成后,浏览器会检查
media条件。如果条件满足(如屏幕宽度变化),该CSS才会被应用,并可能触发重绘。
2. 策略二:异步加载CSS(更主动的控制)
如果你有一些必须加载但又不希望阻塞渲染的CSS(比如首屏非关键CSS),可以使用JavaScript技巧来异步加载。
使用 preload 和 onload(现代、推荐)
preload 告诉浏览器以高优先级获取资源,但不确定如何执行。结合 onload 事件,我们可以在加载完成后将其转换为样式表。
html
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>
rel="preload" as="style":高速浏览器以高优先级去获取这个样式文件,但不会应用它。onload="...":当资源加载完成后,将rel属性改为stylesheet,浏览器就会应用这些样式。<noscript>:为禁用JavaScript的用户提供兜底方案。
动态创建 <link> 标签
使用JavaScript动态创建并插入一个 <link> 标签。通过JS加载的资源默认是异步的。
<script>
// 在<head>底部添加一段脚本
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'non-critical.css';
// 插入到document中开始加载,但不会阻塞渲染
document.head.appendChild(link);
</script>
3. 策略三:内联关键CSS并异步加载其余部分(终极优化)
这是最高效的混合方案,常与“首屏优化”结合。
- 提取关键CSS:使用工具(如
critical、penthouse)将用于首屏内容渲染的CSS提取出来。 - 内联到
<head>:将提取出的关键CSS直接内嵌到HTML的<style>标签中。这消除了一个网络请求,保证了首屏样式立即可用。 - 异步加载完整CSS:然后使用上述策略二的方法异步加载完整的CSS文件。
html
<head>
<style>
/* 这里内联提取出的关键CSS */
body { font-family: sans-serif; }
.hero { color: #333; }
...
</style>
<script>
// 异步加载完整的CSS
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'full-styles.css';
link.media = 'print'; // trick:先设置为print,加载完成后改回all
link.onload = function() {
link.media = 'all';
};
document.head.appendChild(link);
</script>
</head>
4. 策略四:HTTP/2 Server Push(服务器推送)
如果你使用HTTP/2,服务器可以在响应HTML请求时,主动将关键的CSS文件“推送”给浏览器, before the browser even parses the HTML and asks for it. 这可以省去一个网络往返(RTT)时间。
这需要在服务器端进行配置(如Nginx、Node.js)。这是一种高级优化手段,通常与其他策略结合使用。
# Nginx 配置示例
location / {
http2_push /styles/core.css;
...
}
四、如果一个列表有100000个数据,这个该怎么进行展示?
(一) 终极解决方案:虚拟列表 (Virtual List)
这是处理大规模列表渲染的标准答案和唯一推荐的最佳实践。
1. 核心原理
- 只渲染可视区域 (Viewport):计算当前滚动位置,只渲染用户能看到的那部分列表项(比如20-30条)。
- 模拟滚动内容:用一个足够高的容器(通过CSS高度或一个填充元素)来模拟整个长列表的滚动条。
- 动态渲染:监听滚动事件,当用户滚动时,动态计算新的可视区域,销毁移出视口的DOM节点,并创建新进入视口的DOM节点。
2. 实现虚拟列表的关键步骤
HTML 结构:
<div class="viewport" style="height: 500px; overflow-y: auto;">
<div class="list-container" style="height: 10000000px;">
<!-- 这里只动态放置当前可视区域的 item -->
<div class="list-item">Item {
{startIndex}}</div>
<div class="list-item">Item {
{startIndex + 1}}</div>
...
<div class="list-item">Item {
{endIndex}}</div>
</div>
</div>
-
viewport:固定高度的视口,负责产生滚动条。list-container:其高度设置为(总数据量 * 每项高度),用于模拟总滚动范围。.list-item:只有可视区域内的项才会被真实创建和渲染。
JavaScript 逻辑:
-
- 计算总高度:
containerHeight = totalCount * itemHeight - 计算可视区域数量:
visibleCount = Math.ceil(viewportHeight / itemHeight) + buffer(Buffer是缓冲区,多渲染几条防止滚动白屏) - 监听滚动事件:监听
viewport的scroll事件(务必使用节流!)。 - 计算起始索引:
startIndex = Math.floor(scrollTop / itemHeight) - 计算结束索引:
endIndex = startIndex + visibleCount - 计算偏移量:为了保持滚动条正确,需要将可视列表向下偏移
startIndex * itemHeight像素。 - 渲染子集:从
allListData中切片取出[startIndex, endIndex]范围的数据进行渲染。
- 计算总高度:
3. 为什么不自己造轮子?—— 使用现成的库
虚拟列表自己实现起来细节非常多(动态高度、滚动节流、平滑滚动等),强烈推荐使用成熟的库:
- React:
-
- react-window (轻量级,作者是React核心团队)
- react-virtualized (功能更全面,但体积更大)
- Vue:
- Angular:
示例(使用 react-window):
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index} (数据: {data[index]})</div>
);
const Example = () => (
<List
height={500} // 视口高度
itemCount={100000} // 总条数
itemSize={35} // 每项高度
width={'100%'} // 宽度
>
{Row}
</List>
);
几行代码就能完美解决10万条数据渲染问题。
五、怎么统计页面的性能指标?
(一) 核心 Web 指标 (Core Web Vitals)
这是Google提出的、以用户为中心的关键性能指标,现已直接影响搜索引擎排名。
| 指标 |
描述 |
优化目标 |
测量方法 |
| LCP (Largest Contentful Paint) |
测量加载性能。可视区域内最大内容元素(如图片、视频、大文本块)渲染完成的时间。 |
< 2.5s (良好) |
|
| FID (First Input Delay) |
测量交互性。从用户第一次与页面交互(点击链接、按钮等)到浏览器实际能够响应该交互的时间。 |
< 100ms (良好) |
|
| CLS (Cumulative Layout Shift) |
测量视觉稳定性。衡量页面在整个生命周期中发生的所有意外布局偏移的分数。 |
< 0.1 (良好) |
|
如何测量Core Web Vitals:
- 使用 field tools(字段工具 - 真实用户数据):
-
- Chrome UX Report (CrUX):提供来自真实Chrome用户的匿名性能数据。
- Google Search Console:直接报告你网站在核心Web指标上的表现。
使用 web-vitals 库(官方推荐):这是最准确、最简单的方式,它帮你处理了所有兼容性和复杂逻辑。
npm install web-vitals
import {getLCP, getFID, getCLS} from 'web-vitals';
getLCP(console.log);
getFID(console.log);
getCLS(console.log);
// 或发送到你的分析平台
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
- 使用 lab tools(实验室工具 - 模拟环境):
-
- Lighthouse:集成在Chrome DevTools中,或通过命令行、CI运行,提供模拟环境的性能评估和优化建议。
- PageSpeed Insights:结合了Lab Data(Lighthouse)和Field Data(CrUX),给出全面报告。
(二) 传统性能指标 (Navigation Timing API)
performance.timing (已废弃) 和 PerformanceNavigationTiming API 提供了页面加载生命周期的详细时间点。
如何获取这些指标:
// 获取最新的 navigation 条目
const [navigationEntry] = performance.getEntriesByType('navigation');
// 计算关键时间点
const metrics = {
// DNS 查询时间
dnsLookupTime: navigationEntry.domainLookupEnd - navigationEntry.domainLookupStart,
// TCP 连接时间
tcpConnectTime: navigationEntry.connectEnd - navigationEntry.connectStart,
// 请求响应时间(TTFB)
timeToFirstByte: navigationEntry.responseStart - navigationEntry.requestStart,
// 内容加载时间
domContentLoadedTime: navigationEntry.domContentLoadedEventEnd - navigationEntry.domContentLoadedEventStart,
// 页面完全加载时间
fullLoadTime: navigationEntry.loadEventEnd - navigationEntry.loadEventStart,
// 白屏时间 (粗略计算)
whiteScreenTime: navigationEntry.responseStart - navigationEntry.navigationStart,
};
console.table(metrics);
(三) 使用 PerformanceObserver API(现代标准方法)
这是监听各种性能条目的推荐方式,可以异步获取指标,不会阻塞主线程。
示例:监听 LCP
javascript
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP candidate:', entry.startTime, entry);
// 发送到你的监控系统
// sendToAnalytics(entry);
}
});
// 订阅 'largest-contentful-paint' 类型的条目
observer.observe({entryTypes: ['largest-contentful-paint']});
其他可监听的条目类型:
'paint':获取first-paint(FP) 和first-contentful-paint(FCP)。'layout-shift':监听布局偏移,用于计算 CLS。'longtask':监听长任务(耗时超过50ms的任务),判断是否存在阻塞主线程的任务。'resource':监听所有资源(图片、脚本、样式等)的加载性能。
(四) 使用 console.time() 和 console.timeEnd()
对于测量代码块或自定义功能的执行时间非常有用。
console.time('myFunction');
myExpensiveFunction(); // 你要测量的函数
console.timeEnd('myFunction'); // 控制台输出: myFunction: 125.5ms
(五) 实践:构建一个简单的性能监控器
你可以将上述方法组合起来,构建一个轻量级的性能监控脚本。
// perf-monitor.js
(function monitorPerf() {
// 1. 监听 Core Web Vitals (使用 web-vitals 库是更好的选择)
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.startTime);
sendToAnalytics('LCP', entry.startTime);
}
if (entry.entryType === 'layout-shift') {
// 需要累积计算 CLS
console.log('Layout Shift:', entry);
}
});
});
observer.observe({entryTypes: ['largest-contentful-paint', 'layout-shift']});
// 2. 监听首次输入延迟 (FID)
let firstInputReceived = false;
const onFirstInput = (entry) => {
if (!firstInputReceived) {
firstInputReceived = true;
const fid = entry.processingStart - entry.startTime;
console.log('FID:', fid);
sendToAnalytics('FID', fid);
// 移除事件监听器
['mousedown', 'keydown', 'touchstart'].forEach(event => {
document.removeEventListener(event, onFirstInput, true);
});
}
};
['mousedown', 'keydown', 'touchstart'].forEach(event => {
document.addEventListener(event, onFirstInput, { capture: true, once: true });
});
// 3. 获取 Navigation Timing 数据
setTimeout(() => { // 确保在 onload 后执行
const [navigationEntry] = performance.getEntriesByType('navigation');
if (navigationEntry) {
const TTFB = navigationEntry.responseStart - navigationEntry.requestStart;
console.log('TTFB:', TTFB);
sendToAnalytics('TTFB', TTFB);
}
}, 0);
function sendToAnalytics(metricName, value) {
// 这里实现你的数据发送逻辑,例如:
// navigator.sendBeacon('https://your-analytics-endpoint.com', `${metricName}=${value}`);
console.log(`Sending ${metricName}: ${value}`);
}
})();
六、虚拟DOM一定更快吗?
不一定。虚拟DOM并不总是更快,它的主要优势在于为开发者提供了更好的开发体验和可维护性,并在大多数常见场景下提供了“足够好”的性能。
虚拟DOM更像是一种 “性能妥协” 或 “可维护性换性能” 的策略,而不是一种纯粹的性能优化黑魔法。
(一) 虚拟DOM更快的场景(优势)
虚拟DOM在以下情况下通常比直接操作真实DOM更有优势:
- 复杂的、频繁的UI更新:
-
- 场景:一个大型表格的排序、过滤,或者一个具有复杂状态的交互式应用。
- 原因:直接操作DOM需要精确计算哪些节点需要更新、添加、删除。当逻辑变得复杂时,很容易出错或产生冗余操作。虚拟DOM的Diff算法可以自动帮你计算出最小变更集,虽然多了一次Diff的计算开销,但避免了人工计算可能带来的大量不必要的DOM操作。减少并批量化DOM操作带来的收益,远大于虚拟DOM本身Diff计算的开销。
- 声明式编程带来的开发效率提升:
-
- 场景:任何规模的现代Web应用。
- 原因:开发者不需要关心“如何更新DOM”,只需要关心“数据是什么”(状态 => UI)。这极大地降低了代码的复杂度和心智负担,减少了Bug。虽然这本身不是速度优势,但开发效率的提升是巨大的。
- 跨平台渲染:
-
- 场景:React Native、SSR(服务端渲染)、小程序等。
- 原因:虚拟DOM是一个普通的JavaScript对象,它不依赖于浏览器环境。你可以在服务器端根据虚拟DOM生成HTML字符串(SSR),也可以在原生移动端将其渲染为原生组件(React Native)。这是直接操作真实DOM绝对无法做到的。
(二) 虚拟DOM更慢的场景(劣势)
虚拟DOM在以下情况下可能比直接操作真实DOM更慢:
- 极致的性能优化场景:
-
- 场景:需要每秒60帧的高频动画、对延迟极其敏感的拖拽操作、大型数据可视化项目(如Canvas、WebGL)。
- 原因:虚拟DOM的Diff和协调(Reconciliation)过程发生在JavaScript层面,这会占用主线程时间。对于需要极致性能的场景,手动精确控制每一帧的DOM更新,避免任何不必要的JS计算,才是最快的方案。这也是为什么Three.js、D3.js等库通常不采用虚拟DOM的原因。
- 简单的、一次性的静态页面:
-
- 场景:一个简单的宣传页、一个表单提交页面。
- 原因:如果页面几乎没有交互,或者交互非常简单,引入整个虚拟DOM库(如React、Vue)的运行时开销是完全不必要的。直接使用原生JS或轻量级工具会更高效。
- 手动优化到极致的DOM操作:
-
- 场景:一个顶级前端专家为一个特定功能编写了高度优化的原生JS代码。
- 原因:理论上,任何算法都无法比“预先知道 exactly 要做什么”的手动操作更快。虚拟DOM的Diff是“猜测”哪里需要变化,而专家可以直接“命中”目标。但这种情况在大型项目中很少见,且维护成本极高。
(三) 核心:虚拟DOM的价值到底是什么?
让我们用一个简单的比喻来理解:
- 直接操作DOM:像是用汇编语言编程。你可以写出性能极高的代码,但非常繁琐、容易出错、难以维护和协作。
- 使用虚拟DOM:像是用高级语言(如C++/Java) 编程。编译器(虚拟DOM的Diff算法)会帮你生成最终的机器码(DOM操作)。虽然生成的代码可能不是最优的,但它保证了可接受的平均性能和极高的开发效率与可维护性。
虚拟DOM的真正价值在于:
- 提供了性能的下限保障:它通过Diff算法避免了最蠢的DOM操作方式(比如每次更新都重置整个
innerHTML),为开发者提供了一个“还不错”的性能基线。即使是一个新手,用React写出来的应用性能通常也不会太差。 - 解放了开发者:让开发者从手动、繁琐的DOM操作中解脱出来,专注于业务逻辑和状态管理。
- 实现了声明式UI:
UI = f(state)是这个时代最成功的UI开发范式,而虚拟DOM是实现这一范式的高效手段。
七、导致页面加载白屏时间长的原因有哪些,怎么进行优化?
白屏通常发生在页面内容被渲染出来之前的阶段。其核心原因是浏览器正在忙于加载资源、解析、编译和执行,无暇进行渲染。
(一) 白屏时间的阶段分析
要理解原因,首先要知道从输入URL到看到页面内容经历了什么:
- DNS 查询 -> 2. TCP 连接 -> 3. SSL 握手 (HTTPS) -> 4. 发送请求 -> 5. 等待服务器响应 (TTFB) -> 6. 下载 HTML -> 7. 解析 HTML,构建 DOM -> 8. 遇到
<link>和<script>,加载并解析 CSS、JS -> 9. 构建 CSSOM,形成渲染树 -> 10. 布局与绘制
白屏就发生在第1步到第9步完成之前。 任何一步的延迟都会导致白屏时间变长。
(二) 导致白屏时间过长的具体原因及优化方案
我们将原因分为网络层面和浏览器渲染层面。
1. 网络层面原因(资源加载慢)
- 资源体积过大(HTML、JS、CSS)
-
- 原因:巨大的文件需要更长的下载时间。
- 优化:
-
-
- 压缩:使用
Gzip或Brotli压缩文本资源。 - 代码拆分:使用 Webpack 等工具的
import()语法进行动态导入,实现按需加载。 - Tree Shaking:移除 JavaScript 和 CSS 中未使用的代码。
- 优化图片:使用 WebP/AVIF 格式,适当压缩,使用响应式图片(
srcset)。
- 压缩:使用
-
- 网络往返次数多(RTT)
-
- 原因:DNS查询、TCP/SSL握手、重定向、多个小资源请求都会增加RTT。
- 优化:
-
-
- 减少重定向:避免不必要的
3xx重定向。 - 预连接:使用
<link rel="preconnect">或<link rel="dns-prefetch">提前与第三方源建立连接。 - HTTP/2:启用 HTTP/2,利用多路复用特性,减少多个请求的开销。
- CDN:使用CDN将资源分发到离用户更近的节点。
- 减少重定向:避免不必要的
-
- 关键资源加载慢
-
- 原因:阻塞渲染的CSS和JS(特别是放在
<head>中的)下载慢。 - 优化:
- 原因:阻塞渲染的CSS和JS(特别是放在
-
-
- 预加载:使用
<link rel="preload">高优先级加载关键CSS、字体、Logo图片等。 - 缓存:设置合理的
Cache-Control,ETag等缓存策略,减少重复请求。
- 预加载:使用
-
2. 浏览器渲染层面原因(解析和执行慢)
- CSS 阻塞渲染
-
- 原因:浏览器会等待CSSOM构建完成后再进行渲染(避免FOUC)。
优化:
-
-
- 内联关键CSS:将首屏内容所需的关键CSS直接内嵌到HTML的
<style>标签中,消除一次网络请求。 - 异步加载非关键CSS:对非首屏CSS使用
preload或media属性异步加载。
- 内联关键CSS:将首屏内容所需的关键CSS直接内嵌到HTML的
-
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link rel="stylesheet" href="print.css" media="print"> <!-- 不阻塞渲染 -->
- JavaScript 阻塞解析
-
- 原因:
<script>标签会阻塞HTML解析,除非声明为异步。 - 优化:
- 原因:
-
-
- 延迟/异步加载:使用
defer或async属性,避免JS阻塞文档解析。
- 延迟/异步加载:使用
-
-
-
-
defer:脚本异步加载,在DOMContentLoaded事件前按顺序执行。async:脚本异步加载,下载完成后立即执行(执行顺序不定)。
-
-
-
-
- 将非关键JS放在底部:将
<script>标签放在</body>之前。 - 避免同步XHR:绝对不要使用同步的
XMLHttpRequest。
- 将非关键JS放在底部:将
-
- 大量的或复杂的前端框架初始化
-
- 原因:React、Vue等框架需要先加载、解析、执行JS,然后才能开始渲染组件。
- 优化:
-
-
- 服务端渲染 (SSR):这是解决首屏白屏问题的终极武器。在服务器端生成完整的HTML页面下发,浏览器可以直接渲染,然后再由客户端JS“激活”(Hydrate)交互功能。
- 代码分割与懒加载:结合路由进行懒加载,只加载当前页面需要的JS代码。
-
- 长时间运行的主线程任务(Long Tasks)
-
- 原因:复杂的JavaScript计算(如处理大数据)会长时间占用主线程,导致浏览器无法进行渲染。
- 优化:
-
-
- 任务拆分:使用
setTimeout、requestAnimationFrame或Web Worker将长任务拆分成多个小块执行,让主线程有机会进行渲染。
- 任务拆分:使用
-
(三) 优化实战 checklist
你可以按照以下步骤系统地优化白屏时间:
- 测量与分析(首先做!)
-
- 使用 Chrome DevTools 的 Performance 和 Network 面板录制加载过程,找到瓶颈。
- 使用 Lighthouse 或 PageSpeed Insights 获取权威评分和优化建议。关注 FCP (First Contentful Paint) 指标。
- 网络优化
-
- 开启 Gzip/Brotli 压缩。
- 配置强缓存 (
Cache-Control) 和协商缓存 (ETag)。 - 使用 HTTP/2 和 CDN。
- 对关键资源使用
preload<

最低0.47元/天 解锁文章
25万+

被折叠的 条评论
为什么被折叠?



