前端面试题

一、如何优化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的“量”和“复杂度”)

这是最直接优化解析过程的方法。解析一个更小、更简单的树总是更快。

  1. 最小化 DOM 深度和节点数量
    • 原因:更少的节点意味着更少的内存占用、更快的样式计算、更快的布局重排(Reflow)。
    • 做法
      • 避免不必要的包装<div><span>。现代 CSS(如 Flexbox、Grid)可以减少用于布局的冗余标签。
      • 使用语义化标签(如 <article>, <section>, <nav>)而不是一堆 <div>,它们在结构上更清晰。
      • 定期审查代码,移除僵尸节点或注释掉的代码块。
  1. 移除空白和注释
    • 原因:文本节点(包括空白符)也是 DOM 节点。大量的空白和注释会增加不必要的节点数量。
    • 做法:在生产环境中使用构建工具(如 Webpack, Gulp, Vite)对 HTML 进行压缩(Minify),移除所有不必要的字符。
  1. 使用 HTML 惰性加载属性

loading="lazy":对 <img><iframe> 使用此属性。它不会加快初始 DOM 解析,但会推迟这些非关键资源的加载,减少主线程的初始压力,让浏览器更专注于解析和渲染核心内容。

<img src="image.jpg" loading="lazy" alt="...">
 <iframe src="content.html" loading="lazy"></iframe>

(三) CSS 层面的优化(减少对CSSOM的阻塞)

  1. 精简和压缩 CSS
    • 使用工具(如 cssnano、PurgeCSS)移除未使用的 CSS 规则和空白字符。更少的 CSS 规则意味着更快的 CSSOM 构建。
  1. 避免使用 @import
    • 原因@import 会在 CSS 文件中发起一个新的 HTTP 请求,并且是同步的。它会阻止浏览器并行下载其他资源,直到该 @import 的资源被下载和解析。
    • 做法:始终使用 <link> 标签在 HTML 中引入 CSS,这样可以并行下载。
  1. 将 CSS 放在头部(<head>
    • 原因:让浏览器尽早发现 CSS 并开始构建 CSSOM。由于 CSS 不会阻塞 DOM 解析,但会阻塞渲染,尽早加载可以保证浏览器在有 DOM 和 CSSOM 后能立即渲染,减少白屏时间。
  1. 使用媒体查询(Media Queries)
    • 对非立即需要的 CSS(如打印样式、特定屏幕尺寸的样式)使用 media 属性。浏览器会以低优先级下载它们,避免它们阻塞关键渲染路径。
<!-- 关键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 是主要的阻塞源。

  1. 将 JavaScript 放在底部(</body> 之前)
    • 经典方法。将所有的 <script> 标签放在页面内容之后。这样 DOM 的解析基本完成,不会被 JS 所阻塞。
  1. 使用 asyncdefer 属性
    • async (异步)
      • 脚本的下载不会阻塞 HTML 解析。
      • 脚本下载完成后立即执行,此时会阻塞 HTML 解析。
      • 适用于独立且不依赖DOM或其他脚本的第三方脚本(如 analytics)。
    • defer (延迟)
      • 脚本的下载不会阻塞 HTML 解析。
      • 脚本会等到 HTML 解析完全完成后DOMContentLoaded 事件之前),按顺序执行。
      • 适用于需要操作DOM但又不急的脚本,或者有依赖关系的脚本。
<!-- 可能会在中间执行,阻塞解析 -->
<script async src="script.js"></script>

<!-- 保证在最后执行,不阻塞解析 -->
<script defer src="script.js"></script>
  1. 避免使用 document.write()
    • 在现代浏览器中,尤其是在异步或延迟的脚本中,document.write() 会破坏 DOM 结构,并可能导致浏览器执行完整的页面重解析,性能极差。
  1. 使用 requestAnimationFrame setTimeout 拆分长任务
    • 如果必须执行大量的 DOM 操作,不要一次性做完。这会造成主线程长时间被占用,导致页面卡顿。可以将任务拆分成小块,在浏览器的空闲时间执行。
  1. 使用虚拟DOM(Virtual DOM)库
    • 像 React、Vue 这样的框架使用 Virtual DOM。它们先在内存中的 JavaScript 对象(虚拟DOM)上进行更改,然后通过高效的 Diff 算法计算出最小化的变更集,最后再一次性应用到真实 DOM 上。这大大减少了直接操作真实 DOM 的次数,而 DOM 操作是昂贵的。

(五) 其他高级优化

  1. 预加载和预连接
    • 使用 <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">
  1. 服务器端渲染(SSR)
    • 原理:在服务器上生成页面的初始 HTML。用户收到时已经有一个完整的 DOM 结构,JavaScript 再随后激活(Hydrate)交互功能。
    • 优势:极大改善首屏加载时间可交互时间,因为浏览器无需等待所有 JS 下载和执行完就能显示内容。

二、DNS预解析是什么?怎么实现?

(一) 什么是 DNS 预解析?

DNS 预解析(DNS Prefetching) 是一种前端性能优化手段,它允许浏览器在后台提前执行第三方域名的 DNS 解析,从而减少后续实际请求资源时的延迟。

(二) 为什么需要它?—— 解决什么问题

要理解它的价值,我们需要先看一个网络请求的生命周期。当浏览器需要从另一个域名(例如,从 https://www.example.com 去请求 https://cdn.example-network.com/image.jpg)获取资源时,大致需要以下步骤:

  1. DNS 解析:浏览器需要找出 cdn.example-network.com 这个域名对应的真实服务器 IP 地址。这个过程就是 DNS 查询。
  2. TCP 握手:浏览器拿到 IP 后,会与服务器建立 TCP 连接(通常是三次握手)。
  3. TLS 协商(如果是 HTTPS):如果使用 HTTPS,还需要进行 TLS 握手以建立安全连接。
  4. 发送请求 & 接收响应:连接建立后,浏览器才真正发送 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> 标签的 hrefsrc 属性),并自动为这些域名进行 DNS 预解析,甚至不需要开发者手动添加 dns-prefetch 指令。

但是,手动添加仍然在以下情况下至关重要:

  • 非标准或隐藏的资源:通过 JavaScript 动态加载的资源、CSS 中的 @font-face 规则里引用的字体文件、通过 url() 引用的背景图片等。浏览器在初始解析 HTML 时无法发现这些资源,因此不会自动预解析它们的域名。
  • 重定向域名:如果你知道一个 URL 最终会重定向到另一个域名,可以预解析最终的目标域名。
  • 确保高优先级:手动添加可以确保浏览器更早地注意到这些域名并优先处理。

(四) 最佳实践和注意事项

  1. 仅对跨域域名使用:对于你网站自身的域名,DNS 解析通常已经在初始请求页面时完成了,无需再预解析。应专注于第三方域名。
  2. 不要过度使用:每个预解析请求虽然消耗极小的带宽和 CPU,但如果一次性预解析几十个域名,可能会与关键资源争夺网络带宽和 CPU 资源。只预解析最关键、最可能用到的第三方域名
  3. 与 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 属性)

  1. 兼容性
    dns-prefetch 得到了所有主流浏览器的广泛支持,可以放心使用。preconnect 的兼容性也非常好,但略新于 dns-prefetch

三、如何防止CSS阻塞渲染?

(一) 核心结论先行

CSS 默认会阻塞渲染。目标是只让关键CSS阻塞渲染,而对非关键CSS采用异步加载策略,从而让页面内容尽快呈现给用户。

(二) 为什么CSS会阻塞渲染?

浏览器渲染页面的“关键渲染路径”如下:

  1. 构建 DOM:解析 HTML 生成 DOM 树。
  2. 构建 CSSOM:解析 CSS 生成 CSSOM 树。
  3. 合并:将 DOM 和 CSSOM 合并成渲染树(Render Tree)。
  4. 布局:计算每个节点的位置和大小(Layout)。
  5. 绘制:将像素绘制到屏幕上(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技巧来异步加载。

使用 preloadonload(现代、推荐)
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并异步加载其余部分(终极优化)

这是最高效的混合方案,常与“首屏优化”结合。

  1. 提取关键CSS:使用工具(如criticalpenthouse)将用于首屏内容渲染的CSS提取出来。
  2. 内联到 <head>:将提取出的关键CSS直接内嵌到HTML的 <style> 标签中。这消除了一个网络请求,保证了首屏样式立即可用。
  3. 异步加载完整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. 核心原理
  1. 只渲染可视区域 (Viewport):计算当前滚动位置,只渲染用户能看到的那部分列表项(比如20-30条)。
  2. 模拟滚动内容:用一个足够高的容器(通过CSS高度或一个填充元素)来模拟整个长列表的滚动条。
  3. 动态渲染:监听滚动事件,当用户滚动时,动态计算新的可视区域,销毁移出视口的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是缓冲区,多渲染几条防止滚动白屏)
    • 监听滚动事件:监听 viewportscroll 事件(务必使用节流!)。
    • 计算起始索引startIndex = Math.floor(scrollTop / itemHeight)
    • 计算结束索引endIndex = startIndex + visibleCount
    • 计算偏移量:为了保持滚动条正确,需要将可视列表向下偏移 startIndex * itemHeight 像素。
    • 渲染子集:从 allListData 中切片取出 [startIndex, endIndex] 范围的数据进行渲染。
3. 为什么不自己造轮子?—— 使用现成的库

虚拟列表自己实现起来细节非常多(动态高度、滚动节流、平滑滚动等),强烈推荐使用成熟的库:

  • React:
  • 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 (良好)

PerformanceObserver

FID (First Input Delay)
首次输入延迟

测量交互性。从用户第一次与页面交互(点击链接、按钮等)到浏览器实际能够响应该交互的时间。

< 100ms (良好)

PerformanceObserver

CLS (Cumulative Layout Shift)
累积布局偏移

测量视觉稳定性。衡量页面在整个生命周期中发生的所有意外布局偏移的分数。

< 0.1 (良好)

PerformanceObserver

如何测量Core Web Vitals:

  1. 使用 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);
  1. 使用 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更有优势:

  1. 复杂的、频繁的UI更新
    • 场景:一个大型表格的排序、过滤,或者一个具有复杂状态的交互式应用。
    • 原因:直接操作DOM需要精确计算哪些节点需要更新、添加、删除。当逻辑变得复杂时,很容易出错或产生冗余操作。虚拟DOM的Diff算法可以自动帮你计算出最小变更集,虽然多了一次Diff的计算开销,但避免了人工计算可能带来的大量不必要的DOM操作。减少并批量化DOM操作带来的收益,远大于虚拟DOM本身Diff计算的开销。
  1. 声明式编程带来的开发效率提升
    • 场景:任何规模的现代Web应用。
    • 原因:开发者不需要关心“如何更新DOM”,只需要关心“数据是什么”(状态 => UI)。这极大地降低了代码的复杂度和心智负担,减少了Bug。虽然这本身不是速度优势,但开发效率的提升是巨大的。
  1. 跨平台渲染
    • 场景:React Native、SSR(服务端渲染)、小程序等。
    • 原因:虚拟DOM是一个普通的JavaScript对象,它不依赖于浏览器环境。你可以在服务器端根据虚拟DOM生成HTML字符串(SSR),也可以在原生移动端将其渲染为原生组件(React Native)。这是直接操作真实DOM绝对无法做到的。

(二) 虚拟DOM更慢的场景(劣势)

虚拟DOM在以下情况下可能比直接操作真实DOM更慢:

  1. 极致的性能优化场景
    • 场景:需要每秒60帧的高频动画、对延迟极其敏感的拖拽操作、大型数据可视化项目(如Canvas、WebGL)。
    • 原因:虚拟DOM的Diff和协调(Reconciliation)过程发生在JavaScript层面,这会占用主线程时间。对于需要极致性能的场景,手动精确控制每一帧的DOM更新,避免任何不必要的JS计算,才是最快的方案。这也是为什么Three.js、D3.js等库通常不采用虚拟DOM的原因。
  1. 简单的、一次性的静态页面
    • 场景:一个简单的宣传页、一个表单提交页面。
    • 原因:如果页面几乎没有交互,或者交互非常简单,引入整个虚拟DOM库(如React、Vue)的运行时开销是完全不必要的。直接使用原生JS或轻量级工具会更高效。
  1. 手动优化到极致的DOM操作
    • 场景:一个顶级前端专家为一个特定功能编写了高度优化的原生JS代码。
    • 原因:理论上,任何算法都无法比“预先知道 exactly 要做什么”的手动操作更快。虚拟DOM的Diff是“猜测”哪里需要变化,而专家可以直接“命中”目标。但这种情况在大型项目中很少见,且维护成本极高。

(三) 核心:虚拟DOM的价值到底是什么?

让我们用一个简单的比喻来理解:

  • 直接操作DOM:像是用汇编语言编程。你可以写出性能极高的代码,但非常繁琐、容易出错、难以维护和协作。
  • 使用虚拟DOM:像是用高级语言(如C++/Java) 编程。编译器(虚拟DOM的Diff算法)会帮你生成最终的机器码(DOM操作)。虽然生成的代码可能不是最优的,但它保证了可接受的平均性能极高的开发效率与可维护性

虚拟DOM的真正价值在于:

  1. 提供了性能的下限保障:它通过Diff算法避免了最蠢的DOM操作方式(比如每次更新都重置整个innerHTML),为开发者提供了一个“还不错”的性能基线。即使是一个新手,用React写出来的应用性能通常也不会太差。
  2. 解放了开发者:让开发者从手动、繁琐的DOM操作中解脱出来,专注于业务逻辑和状态管理。
  3. 实现了声明式UIUI = f(state) 是这个时代最成功的UI开发范式,而虚拟DOM是实现这一范式的高效手段。

七、导致页面加载白屏时间长的原因有哪些,怎么进行优化?

白屏通常发生在页面内容被渲染出来之前的阶段。其核心原因是浏览器正在忙于加载资源、解析、编译和执行,无暇进行渲染。

(一) 白屏时间的阶段分析

要理解原因,首先要知道从输入URL到看到页面内容经历了什么:

  1. 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)
    • 原因:巨大的文件需要更长的下载时间。
    • 优化
      • 压缩:使用 GzipBrotli 压缩文本资源。
      • 代码拆分:使用 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>中的)下载慢。
    • 优化
      • 预加载:使用 <link rel="preload"> 高优先级加载关键CSS、字体、Logo图片等。
      • 缓存:设置合理的 Cache-Control, ETag 等缓存策略,减少重复请求。
2. 浏览器渲染层面原因(解析和执行慢)
  • CSS 阻塞渲染
    • 原因:浏览器会等待CSSOM构建完成后再进行渲染(避免FOUC)。

优化

      • 内联关键CSS:将首屏内容所需的关键CSS直接内嵌到HTML的<style>标签中,消除一次网络请求。
      • 异步加载非关键CSS:对非首屏CSS使用 preloadmedia 属性异步加载。
<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解析,除非声明为异步。
    • 优化
      • 延迟/异步加载:使用 deferasync 属性,避免JS阻塞文档解析。
        • defer:脚本异步加载,在 DOMContentLoaded 事件前按顺序执行。
        • async:脚本异步加载,下载完成后立即执行(执行顺序不定)。
      • 将非关键JS放在底部:将 <script> 标签放在 </body> 之前。
      • 避免同步XHR:绝对不要使用同步的 XMLHttpRequest
  • 大量的或复杂的前端框架初始化
    • 原因:React、Vue等框架需要先加载、解析、执行JS,然后才能开始渲染组件。
    • 优化
      • 服务端渲染 (SSR)这是解决首屏白屏问题的终极武器。在服务器端生成完整的HTML页面下发,浏览器可以直接渲染,然后再由客户端JS“激活”(Hydrate)交互功能。
      • 代码分割与懒加载:结合路由进行懒加载,只加载当前页面需要的JS代码。
  • 长时间运行的主线程任务(Long Tasks)
    • 原因:复杂的JavaScript计算(如处理大数据)会长时间占用主线程,导致浏览器无法进行渲染。
    • 优化
      • 任务拆分:使用 setTimeoutrequestAnimationFrameWeb Worker 将长任务拆分成多个小块执行,让主线程有机会进行渲染。

(三) 优化实战 checklist

你可以按照以下步骤系统地优化白屏时间:

  1. 测量与分析(首先做!)
    • 使用 Chrome DevToolsPerformanceNetwork 面板录制加载过程,找到瓶颈。
    • 使用 LighthousePageSpeed Insights 获取权威评分和优化建议。关注 FCP (First Contentful Paint) 指标。
  1. 网络优化
    • 开启 Gzip/Brotli 压缩。
    • 配置强缓存 (Cache-Control) 和协商缓存 (ETag)。
    • 使用 HTTP/2 和 CDN。
    • 对关键资源使用 preload<
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值