在当今数字时代,网站的性能对于吸引和保留用户至关重要。用户不愿意等待缓慢的加载时间,而快速响应的页面将帮助您留住访问者,提升转化率。前端性能优化是实现这一目标的关键因素之一。在本文中,我们将探讨一些重要的前端性能优化策略,以提高网站速度、交互性和用户满意度。
img

测量工具

现在将以 Performance Observer 为例,详细讨论几个重要的性能指标的具体实现

重要指标

LCP

  • 指视图中最大图像和文本块呈现的时间。您可以通过查看截图来了解其标准。如欲深入了解,请点击这里

LCP 分析会考虑到其找到的所有内容,甚至包括已从 DOM 中删除的内容。每当发现新的最大内容时,它都会创建一个新条目,因此可能会存在多个对象。然而,当发生滚动或输入事件时,LCP 分析会停止搜索更大的内容。因此,一般来说,LCP 数据会取最后一个找到的内容作为结果。

1
2
3
4
5
6
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(lastEntry);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });

以下是对于给定指标的解释和展示对象的描述:

  • element : 当前最大的内容绘制元素
  • loadingTime: 加载时间
  • renderTime: 渲染时间。如果是跨域请求,则为 0。
  • size: 元素本身的面积
  • startTime: 如果 renderTime 不为 0,则返回 renderTime;否则返回 loadingTime

在这个示例中,LCP 为 loadingTime,即 1.6。根据上述度量标准,这被认为是良好的。这表示在视图中最大的内容元素(图片)在 1.6 秒内成功渲染,符合较好的用户体验标准。

FCP

  • 指的是任何内容的一部分首次在屏幕上呈现的时间。您可以通过查看截图来了解其标准。如果想深入了解,可以点击这里。与之类似的还有一个指标是 FP(First Paint) ,表示第一个像素绘制到屏幕上的时间。”

1
2
3
4
5
6
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});
observer.observe({ type: 'paint', buffered: true });


在给定的指标中:

  • duration: 持续时间,这里是 0。
  • startTime: 返回绘制发生时的时间戳。

在本示例中,FCP 为startTime ,即小于 1 秒。根据提供的标准,这被认为是良好的。

FID

  • 指的是用户首次与页面交互到浏览器实际能够开始处理程序以响应该交互的时间。FID 测量接收到输入事件与主线程下次空闲之间的增量。即使在未注册事件侦听器的情况下,也会测量 FID。此外,FID 仅关注离散事件操作,例如点击、触摸和按键等。与之不同的是,缩放和滚动以及持续性事件(如 mousemove、pointermove、touchmove、wheel 和 drag)不包括在这个指标中。详细了解可以点击这里

1
2
3
4
5
6
7
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
const FID = entry.processingStart - entry.startTime;
console.log(entry);
});
});
observer.observe({ type: 'first-input', buffered: true });


在给定的指标中:

  • duration: 表示从 startTime 到下一个渲染绘制的时间。
  • processingStart : 测量用户操作与事件处理程序开始运行之间的时间。
  • processingEnd : 测量事件处理程序运行所花费的时间。
  • target: 返回关联事件的 DOM。

在示例代码中,FID 等于 8574(processingEnd) - 8558(processingStart) = 16。根据提供的标准,这被认为是良好的。

INP

  • 指标通过观察用户在页面生命周期内发生的所有点击、触摸和键盘交互的延迟来评估页面对用户交互的整体响应能力。最终的 INP 值是观察到的最长交互,忽略异常值。INP 将于 2024 年 3 月 12 日取代 FID,成为核心 Web Vitals 指标。


NIP 仅会受到以下事件的影响:

  • 用鼠标点击
  • 点击带有触摸屏的设备
  • 按下物理键盘或屏幕键盘上的某个键

与 FID 的关系:

INP 考虑了所有页面交互,而 FID 仅考虑第一次交互。INP 不仅仅关注于首次交互,而是通过对所有交互进行抽样,以全面评估响应能力,使 INP 成为比 FID 更可靠的整体响应能力指标。

由于 Performance API 中没有提供 INP 的响应能力,因此这里不提供具体示例。有关如何测量此指标的信息,请点击这里

CLS

  • 衡量页面在其整个生命周期内发生的最大意外布局偏移分数。在此评估中,仅考虑元素改变其初始位置的情况,对于增加新元素到 DOM 或元素的宽度、高度改变等情况不予以计算。详情了解可以点击这里
1
2
3
4
5
6
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});
observer.observe({ type: 'layout-shift', buffered: true });


其中有几个指标:

  • value: 返回偏移分数,计算方式为:layout shift score = impact fraction \* distance fraction。
  • hadRecentInput: 如果 lastInputTime 过去小于 500 毫秒,则返回 true。
  • lastInputTime: 返回最近排除的用户输入的时间,如果没有则返回 0。仅考虑那些用户未期望的偏移,例如在响应用户互动时发生的离散事件,如点击链接、点击按钮或请求 API 时显示加载等。这些情况被视为合理的偏移。

在本示例中,CLS 为 value, 即 0。根据提供的标准,这被认为是良好的。。

Long Task

  • 阻塞主线程超过 50 毫秒的长任务,可能导致多种不良影响,包括响应事件的延迟和动画卡顿。当主线程被长任务占用时,浏览器无法及时响应用户输入和处理其他事件,从而影响了用户体验。

主要原因可能是:

  • 长时间运行的事件处理程序(Long-running event handlers)
  • 昂贵的回流(reflow)和其他重新渲染操作,例如 DOM 操作、动画等
  • 超过 50 毫秒的长时间循环
1
2
3
4
5
6
7
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});

observer.observe({ type: 'longtask', buffered: true });


其中几个指标解释如下:

  • duration:表示任务的持续时间,即从开始到结束所经过的时间。
  • TaskAttributionTiming:是与长任务(Long Task)相关的对象,用于追踪和归因长任务的执行。这个对象可能包含关于长任务的详细信息,例如任务的来源、触发事件等。通过这个对象,开发人员可以更好地了解长任务的上下文和原因,从而进行性能优化和调试。

因为长任务(Long Task)对用户体验有显著影响,所以即使它不是 Web Vitals 的一部分,也将其单独列出来

优化措施

代码分割和懒加载

懒加载和代码分割都是用于优化前端性能的策略,但它们有不同的目标和应用方式。

代码分割(Code Splitting)

目标:代码分割的主要目标是减小初始加载时所需的 JavaScript 文件大小,以提高页面的初始加载速度。它将应用的代码分成多个块,通常基于路由或功能,以便按需加载。

懒加载(Lazy Loading):

目标:懒加载是一种策略,它使您能够将某些组件、资源或功能推迟加载,而不是在初始加载时加载它们。这有助于减少初始加载时的资源负担,提高页面速度。

应用

在 React 中,这一概念主要体现在将代码拆分(code split)与懒加载(lazy load)相结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const DownloadFile = lazy(() => import('./page/OperateFile/OperateFile'));
const TimeSelect = lazy(() => import('./page/TimeSelect/TimeSelect'));

export const router: Router[] = [
{
path: '/',
element: <App />,
name: 'Home',
},
{
path: '/download-file',
element: <DownloadFile />,
name: 'Download File',
},
{
path: '/time-select',
element: <TimeSelect />,
name: 'Time Select',
},
];

Http 缓存

通常,我们根据资源的更新频率来配置合适的指令,以从缓存中获取资源,从而降低请求频率并提升加载效率。这涉及到针对响应(response)、请求(request)的具体配置。具体的配置指令可能因情况而异,详细内容请点击这里

  1. 针对 HTML 文件更新频率高,设置指令为:
1
Cache-Control: no-cache

表示会缓存,但在使用之前将先向服务器验证是否为最新数据。如果客户端已经是最新的,通常响应将返回 304(Not Modified);反之,将使用新的数据。这种做法确保每次获取的都是最新的响应。由于大多数 HTTP 1.0 不支持 no-cache,我们可以采用一种 fallback 方案。

1
Cache-Control: max-age=0, must-revalidate

这里有一个额外的细节,通常我们还会添加以下信息:如果资源属于用户个人内容,可以将其指定为 private;反之则为 public。判断资源是否为个人数据的方法之一是查看请求头中是否包含 Authorization 字段,如果有,则意味着这是个人数据,通常就无需额外指定为 private 了。此外,如果缓存控制头中包含 must-revalidate,也标识这是个人数据。这表示每次获取资源之前需要验证其是否为新的资源,如果是,就使用新的;如果不是,则使用已缓存的旧数据。这种方式有助于确保对个人数据的实时性和一致性

  1. 对于前端静态资源,例如打包后的脚本和样式文件,通常在文件名后添加一串 hash 或版本号,这有助于更有效地管理缓存。对于这类静态文件,我们通常会设置以下缓存指令:
1
2
3
Cache-Control: public, immutable, max-age=31536000
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
  • Max-age:该指令可在 request 和 response 中添加,用于指示资源的缓存过期时间,以秒为单位。例如,在示例中,max-age=31536000 表示资源在客户端缓存中将在 365 天后过期。
  • Immutable:存在于 response 中,表明资源在最新版本之前将不会更新。在这个例子中,如果静态文件的 hash 或版本发生变化,这个资源就会被视为新的,从而触发重新获取和存储。这种模式被称为cache-busting
  • ETag:用于标识资源是否为指定版本,而 Last-Modified 是 ETag 的后备,表示服务器端最后修改的时间。ETag 和 Last-Modified 允许客户端向服务器发送 condition request,如果资源没有更改,则返回 304 告知缓存版本仍然是最新的;否则,重新发送请求从服务器获取资源。

ETag、Last-Modified 和 Immutable 可以阻止资源重新验证,尤其在重新加载页面时。这些机制有助于优化缓存管理,确保资源的一致性和有效性。

  1. 对于其他资源,例如 favicon.ico、图片、API endpoints 等,通常采用类似以下的设置,并通过 Last-Modified 和 ETag 发起条件请求,以检查资源是否为最新。
1
2
3
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE

在一些场景中,Cache-Control 可能会同时出现在 request 和 response 上,而在发生冲突时,通常会以 response 上的设置为准。

CDN

内容分发网络(CDN)是一个分布式的服务器网络,它缓存来自源服务器的资源,并通过更接近用户地理位置的服务器提供这些资源。通过降低往返时间(RTT),以及采用 HTTP/2 或 HTTP/3、缓存和压缩等优化策略,CDN 能够更快地提供内容,改善用户的访问体验,具体可以点击这里了解。

最小化代码

JavaScript

关于 代码的最小化和压缩,目前我们使用 Terser 工具来实现,主要包括移除未使用的代码(Tree Shaking)、缩短变量名以及删除空格、Uglifiers 等操作。这一优化手段在 Rollup.js 和 Webpack 中都得到了应用,以降低代码体积、减少下载时间。

Style

对于 CSS,在 Webpack 中,通常会利用mini-css-extract-plugin** 插件进行优化。该插件能够独立地从每个包含 CSS 的 JavaScript 文件中提取出一个单独的 CSS 文件,实现样式的独立加载。更进一步,该插件支持按需加载和 Source Map,为样式管理提供了更加灵活和高效的方式。

Image source

对于图片资源,使用 WebP 格式代替 JPEG 和 PNG 可以显著减少文件大小,通常能够实现 25%-35% 的减少。同时,使用内容交付网络(CDN)对于图片加载的优化效果显著,通常能够实现 40%-80%的图片大小节省。为了考虑兼容性,可以采用以下方式来实现

1
2
3
4
5
<picture>
<source type="image/webp" srcset="flower.webp">
<source type="image/jpeg" srcset="flower.jpg">
<img src="flower.jpg" alt="">
</picture>

捆绑优化(bunding optimization)

HTTP/1.0、HTTP/1.1 和 HTTP/2.0 比较

前端每次请求资源时,涉及建立一个 TCP 连接,完成请求后即会关闭该 TCP 连接。如下图所示:

HTTP 协议经历了多次版本的更新,主要包括 HTTP/1.0、HTTP/1.1 和 HTTP/2.0。以下是不同版本的 HTTP 在请求方面的一些关键差异,以图表形式展示:

对于 Web 开发者而言,采用 HTTP/3 并未带来过多的变化,因为 HTTP/3 仍然遵循 HTTP 协议的核心原则。通过 QUIC(Quick UDP Internet Connections)协议的支持,HTTP/3 在连接建立时延方面提供了更低的延迟,改善了多路复用效率,并引入更灵活的流控制机制。鉴于这些优势,HTTP/3 在性能方面有所提升。然而,由于 HTTP/3 的实现主要发生在协议层面,对于 Web 开发者来说,通常无需进行大规模的应用程序更改,所以并没有把 HTTP/3 列入比较。。

Server Push

同时值得注意的是,HTTP/2.0 引入了 Server Push 功能,这对于改善前端性能非常有利。Server Push 允许服务器主动将资源推送给前端,例如,在客户端请求 HTML 文件时,服务器可以直接将 CSS 和 JavaScript 资源主动推送给客户端,省去了客户端发起请求的时间。

然而,需要注意的是,由于 Server Push 机制的一些限制,目前 Chrome 浏览器并不支持 HTTP/2 Server Push 功能。详细的支持情况可以查看此链接。尽管如此,开发者仍然可以利用其他性能优化手段,例如资源合并、缓存策略等,以提高前端加载性能

结论

上述描述表达了 HTTP 协议发展的演进,其目的都在于缩短加载时间,提高请求效率。在这一过程中,出现了一些传统的性能优化技术,例如资源内联(resource inlining)和图像精灵(image spriting),它们通过将多个小文件捆绑成一个大文件,并在单个连接上传输,有助于减少传输的头部开销,从而提高性能。在 HTTP/1.0 和 HTTP/1.1 时代,这样的技术被认为是有效的性能优化实践。

然而,随着 HTTP/2.0 的引入,这一情况发生了变化。HTTP/2.0 允许在同一连接上同时请求多个资源,而无需每个资源都建立独立的 TCP 连接。这一特性使得捆绑优化等“hack”技术在 HTTP/2.0 时代变得不再那么必要,因为单一连接上的多路复用大大提高了资源的并行传输效率。因此,在 HTTP/2.0 时代,我们不再迫切需要依赖这些传统的性能优化技术,而可以更专注于其他方面的性能优化,以更好地适应新的协议特性。在项目中,很多开发者可能已经减少或不再使用这样的技术,而将注意力集中在更为有效的性能优化手段上。

渲染阻塞资源(Render-blocking resources)

渲染路径如下图所示,可以观察到 CSS 和 JavaScript 会阻塞渲染,因此需要根据业务的重要性来识别并优化关键资源的加载顺序,以提升加载时间。目前存在一个非标准的属性 blocking=render,允许开发者明确地将<link><script><style> 元素标记为在该元素被处理之前阻塞渲染,但同时允许解析器在此期间继续处理文档

Browser Resource Hint

帮助开发者通过告知浏览器如何加载和设置资源优先级,进一步优化页面加载时间,具体操作如下:

  • prefetch: 用于提示浏览器预先请求并缓存未来可能会在子页面加载时使用的资源,以缩短加载时间。这一机制具有较低的优先级,适用于主线程空闲时进行的资源获取。
  • dns-prefetch: 用于优化解析域名到 IP(DNS Lookup)的时间,特别适用于加载第三方域名下资源的场景
  • preconnect: 涵盖了 DNS 查询、TLS 协商以及 TCP 握手等步骤,更彻底地准备连接到远程服务器。

为了兼容性,建议结合使用 DNS Prefetch 和 preconnect,但需要谨慎配置,避免过度使用以防资源浪费

1
2
<link rel="preconnect" href="https://third-party-domain.com" />
<link rel="dns-prefetch" href="https://third-party-domain.com" />

测试效果如图

  • prerender: 功能类似于 prefetch,但不同之处在于它会预渲染整个页面,而不仅仅是特定的一些资源。
  • preload: 暗示浏览器尽快下载资源,通常用于需要提前下载的一些`重要资源`,如关键 CSS 或影响 LCP 的图片等。

Defer vs async

asyncdefer 允许外部脚本在加载时不阻塞 HTML 解析器,而带有type="module" 的脚本(包括内联脚本)会自动被延迟执行。

###Fetch Priority API**

您可以通过 Fetch Priority API 的 fetchpriority属性来提高资源的优先级。您可以在<link><img><script>元素中使用该属性。

  • high:以较高的优先级获取图像,相对于其他图像而言。
  • low:以较低的优先级获取图像,相对于其他图像而言。
  • auto:默认模式,表示对获取优先级没有偏好。浏览器决定对用户最有利的方式。

Img

Attributes

  1. loading 属性告知浏览器如何加载图片。
    • eager:立即加载图片,无论其是否可见。
    • lazy:延迟加载图片,直到图片出现在视口,可节省带宽。建议同时为图片添加宽高属性。
  2. fetchpriority属性可指定图片加载的优先级

通过根据图片的业务价值使用这些属性,可以优化 Web Core Vitals 指标,提升整体性能。此外,提前加载关键图片资源也可使用link** 标签。

1
<link rel="preload" fetchpriority="high" as="image" href="image.webp" type="image/webp">
  1. size:
    • 图像不应提供大于用户屏幕上呈现的版本。
    • 使用响应式图像,指定多个图像版本,浏览器会选择使用最佳版本。
    1
    <img src="flower-large.jpg" srcset="example-small.jpg 480w, example-large.jpg 1080w" sizes="50vw">

    480w 是指告知浏览器在不需要下载图片的情况下,就知道宽度是 480px;
    sizes 指定图片预期显示大小
    可以使用 svg,可以无限缩放

  2. width 和 height
    • 应该同时指定适当的widthheight 属性,以确保浏览器在布局中分配正确的空间。这有助于避免布局偏移,提高 Cumulative Layout Shift(CLS)的用户体验。
    • 如果无法确定具体的宽度和高度,可以考虑设置宽高比例,以提供一种解决方案
    1
    2
    3
    4
    5
    img {
    aspect-ratio: 16 / 9;
    width: 100%;
    object-fit: cover;
    }
  3. Decoding
    该属性提供了对浏览器的提示,指示它应该如何解码图像。更具体地说,它指定是等待图像解码完成后再呈现其他内容更新,还是允许在解码过程中同时呈现其他内容。

    -   {% mark sync color:green %}:同步解码图像,以便与其他内容一同呈现。
    -   {% mark async color:green %}:异步解码图像,并允许在其完成之前呈现其他内容。
    -   {% mark auto color:green %}:对解码模式没有偏好;浏览器决定对用户最有利的方式。这是默认值,但不同的浏览器有不同的默认值:
    
    Chromium 默认为 sync,Firefox 默认为 async,Safari 默认为 sync。`decoding` 属性的效果可能仅在非常大、高分辨率的图像上才会显著,因为这些图像的解码时间较长。
    

图像格式及应用场景:

对于图片资源,需要根据具体业务需求来选择合适的图像格式以优化性能。
以下是简化和优化表达的建议:

  • 光栅图像(Raster images):表示为像素网格的照片,包括 GIF、PNG、JPEG 和 WebP。
  • 矢量图像(Vector images):主要用于 logo 和 icon,通过曲线、线条和形状定义,与分辨率无关,可提供清晰的结果。

主要的图片格式:

  • JPEG:适用于摄影图片,通过有损和无损优化减小文件大小。
  • SVG:用于 icon 和 logo,包含几何图形,无论缩放如何都保持清晰。
  • PNG:适用于高分辨率图片,无损压缩,而 WebP 整体上更小。
  • Video:对于动画,建议使用 video 而不是 GIF,因为 GIF 有颜色限制且文件大小较大。

Video

Preload

属性是为了向浏览器提供有关作者认为在视频播放之前加载哪些内容会导致最佳用户体验的提示。它可以具有以下值:

  • none: 表示视频不应预加载。
  • metadata: 表示仅获取视频元数据(例如长度)。
  • auto: 表示整个视频文件可以下载,即使用户预计不会使用它。
  • 空字符串: auto 值的同义词。

每个浏览器的默认值不同。规范建议将其设置为 metadata。具体来说比如想要推迟视频的加载,可以写成这样:

1
2
3
4
5
6
7
<video controls preload="none" poster="placeholder.jpg">
<source src="video.mp4" type="video/mp4">
<p>
Your browser doesn't support HTML video. Here is a
<a href="myVideo.mp4" download="myVideo.mp4">link to the video</a> instead.
</p>
</video>

使用 video 代替 gif

在相同的视觉质量下,视频文件通常比 GIF 图像更小。以下示例展示了懒加载视频并自动播放。通过使用 IntersectionObserver 监测视频是否进入可视范围,并在需要时进行加载和播放。这样做可以提高首次加载的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//playsinline 兼容自动播放在ios上
//poster 一个video的placeholder
<video class="lazy" autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
<source data-src="one-does-not-simply.webm" type="video/webm">
<source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>
document.addEventListener("DOMContentLoaded", function() {
var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

if ("IntersectionObserver" in window) {
var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(video) {
if (video.isIntersecting) {
for (var source in video.target.children) {
var videoSource = video.target.children[source];
if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
videoSource.src = videoSource.dataset.src;
}
}

video.target.load();
video.target.classList.remove("lazy");
lazyVideoObserver.unobserve(video.target);
}
});
});

lazyVideos.forEach(function(lazyVideo) {
lazyVideoObserver.observe(lazyVideo);
});
}
});

如果视频作为 Largest Contentful Paint (LCP) 元素,可以预先请求 poster placeholder 图片,这样有助于提升 LCP 性能。

1
<link rel="preload" as="image" href="poster.jpg" fetchpriority="high">

Pre-render

SSR(Server Side Render)

在服务器上执行客户端应用程序逻辑,并生成包含完整 HTML 标记的响应,以响应 HTML 文档的请求。通过在服务端请求相关资源文件,SSR 提高了首屏加载速度并增强了搜索引擎优化(SEO)效果。尽管 SSR 需要额外的服务器处理时间,并且每次重新请求都需要重新生成,但通常这种权衡是值得的。因为服务器处理时间是在开发者的控制范围内,而用户的网络和设备性能则不可控制。在实践中,SSR 的优势往往超过了其缺点,特别是在考虑到改善用户体验和搜索引擎排名的情况下

SSG(Static-Site Generation)

在构建时编译和呈现网站程序的过程。它生成一系列静态文件,包括 HTML 文件、JavaScript 和 CSS 等资产。这些静态文件在每次请求时被重复利用,无需重新生成。通过将静态生成的页面缓存到 CDN 中,可以在不需要额外配置的情况下提高性能。

SSG 的主要应用场景是对于所有用户而言,渲染的页面内容都是相同的。因此,对于博客、文档站点等内容相对固定的网站,SSG 是一种非常合适的方式。在构建时进行预渲染,生成静态文件,使得这些文件可以被缓存,提供快速的访问体验。这种静态生成的方式适合不经常变化的内容,从而减少了服务器运行时的负担,同时提供了更好的性能。

优化 Javascript Execution

对于 UI 改动,推荐使用requestAnimationFrame

浏览器会在下次重绘时调用该方法,相较于 setIntervalsetTimeout,它能够更智能地在浏览器的帧渲染中进行优化。使用 setIntervalsetTimeout 有可能导致回调在帧的某个点运行,可能在帧的末尾,这通常导致错过一帧,从而导致界面卡顿。而 requestAnimationFrame可以确保回调在浏览器准备好进行下一次重绘时执行,使得动画效果更加流畅。

避免长任务,代码优化

Long Task 指的是执行时间超过 50 毫秒的任务,可以通过以下方式在 Main Thread 上释放:

  • Web Workers 是在后台运行的独立线程,拥有自己的堆栈、堆内存和消息队列。与主线程进行通信只能通过postMessage** 方法发送消息,而无法直接操作 DOM。因此,Web Workers 极为适合执行那些不需要与 DOM 直接交互的任务。例如,对大规模数据进行排序、搜索等操作可以放在 Web Worker 中执行,从而避免了这些计算密集型任务对主线程的阻塞,确保主线程保持响应性。通过将这些耗时任务放在 Web Worker 中执行,不仅可以提高主线程的性能和响应性,还能更好地利用多核处理器的性能优势。这种分离计算任务与用户界面操作的方式有助于改善整体的用户体验,确保页面流畅运行。

  • Service Worker 是一种在后台运行的脚本,用于拦截和处理网络请求。通过合理利用 Service Worker,可以对资源进行缓存,从而减少对主线程的依赖,提高应用程序的性能。

  • 为了确保长时间运行的任务不会阻塞主线程,我们可以采用将这些长任务拆分成小的、异步执行的子任务的策略。可以采用一下策略:

    1. 使用requestIdleCallback** 是一种优化手段,可在 main thread 空闲时调度执行低优先级或后台任务,以提高页面的响应性。这种方法有助于确保任务的执行不会干扰用户交互和页面渲染,而是在主线程空闲时进行。
    2. 手动推迟代码执行的同时,会面临任务被放到队列的最后,并不能直接指定优先级的问题,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function yieldToMain() {
    //Wrapping with Promise is for presenting it in a synchronous manner."
    return new Promise((resolve) => {
    setTimeout(resolve, 0);
    });
    }

    //isInputPending is true when user attempts to interact with the page
    // performance.now() >= deadline is isInputPending fallback
    if (
    navigator.scheduling?.isInputPending() ||
    performance.now() >= deadline
    ) {
    await yieldToMain();
    deadline = performance.now() + 50;
    continue;
    } else {
    otherTask();
    }
    1. scheduler.postTask 允许以更细粒度的方式调度任务,并且是一种帮助浏览器确定任务优先级的方法,确保低优先级任务可以释放 main thread 的机制。尽管目前大多数浏览器并不全面支持,但可在这里获取详细信息。

    2. scheduler.yield 是用于释放主线程的机制,具体详情可参考这里

    需要注意的是微任务并不会释放主线程。例如,当使用 Promise 创建一个微任务时,它会被放入微任务队列中,等待主线程执行完毕后立即执行。即使通过 queueMicrotask 创建的微任务,也会作为第一个执行。这导致主线程会在执行微任务时保持繁忙,不会释放去执行其他任务。

    详细的可视化展示可以在这里查看

    这种机制在处理异步任务时非常重要,因为它确保微任务中的逻辑在当前任务结束后立即执行。这对于处理 Promise 或其他异步操作的结果非常有用,但需要注意它并不会释放主线程。

    1. 批处理

    举例来说,React 的虚拟 DOM 机制采用了批处理的优化策略。它通过将所有变化应用于虚拟 DOM,然后一次性提交给浏览器进行重绘,从而极大地减少了对实际 DOM 的操作。这种方式有效释放主线程,提升性能。这样的批处理机制在 DOM 操作较多或变化频繁的情况下,通过将多个操作合并为一个批次来减少浏览器的重绘次数,从而优化了性能。在 React 中,这一机制有助于提高页面的响应性,避免不必要的重复计算和渲染。

结语

前端性能优化是一个持续的过程,需要不断的关注和改进。通过综合考虑上述策略,您可以提高网站的速度、交互性和用户满意度。不断关注性能,使用工具来评估和监测网站性能,然后采取适当的措施来改进,将有助于确保您的网站始终能够提供出色的用户体验。在当今竞争激烈的互联网环境中,前端性能优化是取得成功的不可或缺的一环。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

蜀ICP备2025133850号