前端性能优化知识点汇总

1,806 阅读24分钟

1.为什么要做性能优化

前端页面性能是影响用户体验的关键因素,用户打开网站时间太久、操作卡顿等,都会导致用户离开页面,严重影响用户的留存率。Google DoubleClick 研究表明:如果一个移动端页面加载时长超过 3 秒,用户就会放弃而离开。BBC 发现网页加载时长每增加 1 秒,用户就会流失 10%。

那么,如何去衡量页面的性能,这就需要性能指标。

2.性能指标

以用户为中心的性能指标

  • First Paint 首次绘制(FP)
  • First contentful paint 首次内容绘制 (FCP)
  • Largest contentful paint 最大内容绘制 (LCP)
  • First input delay 首次输入延迟 (FID)
  • Time to Interactive 可交互时间 (TTI)
  • Total blocking time 总阻塞时间 (TBT)
  • Cumulative layout shift 累积布局偏移 (CLS)

FP & FCP

  • FP(First Paint):首次绘制,这个指标用于记录页面第一次绘制像素的时间
  • FCP(First Contentful Paint):首次内容绘制,这个指标用于记录页面首次绘制文本、图片、非空白 Canvas 或 SVG 的时间,也包括带有正在加载中的 Web 字体的文本

FP 指的是绘制像素,比如说页面的背景色是灰色的,那么在显示灰色背景时就记录下了 FP 指标。

但是此时 DOM 内容还没开始绘制,可能需要文件下载、解析等过程,只有当 DOM 内容发生变化才会触发,比如说渲染出了一段文字,此时就会记录下 FCP 指标。

我们可以把这两个指标认为是和白屏时间相关的指标。

LCP

最大内容绘制,LCP(Largest Contentful Paint),用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。指标变化如下图:

LCP 其实能比前两个指标更能体现一个页面的性能好坏程度,因为这个指标会持续更新。举个例子:当页面出现骨架屏或者 Loading 动画时 FCP 其实已经被记录下来了,但是此时用户希望看到的内容其实并未呈现,我们更想知道的是页面主要的内容是何时呈现出来的。

此时 LCP 指标是能够帮助我们实现想要的需求的。

TTI

首次可交互时间,TTI(Time to Interactive)。这个指标计算过程略微复杂,它需要满足以下几个条件

  1. 从 FCP 指标后开始计算
  2. 持续 5 秒内无长任务(执行时间超过 50 ms)且无两个以上正在进行中的 GET 请求
  3. 往前回溯至 5 秒前的最后一个长任务结束的时间

image.png

这里你可能会疑问为什么长任务需要定义为 50ms 以外?

Google 提出了一个 RAIL 模型:

image.png

对于用户交互(比如点击事件),推荐的响应时间是 100ms 以内。那么为了达成这个目标,推荐在空闲时间里执行任务不超过 50ms(W3C 也有这样的标准规定),这样能在用户无感知的情况下响应用户的交互,否则就会造成延迟感。

FID

首次输入延迟,FID(First Input Delay),记录在 FCP 和 TTI 之间用户首次与页面交互时响应的延迟。

这个指标其实挺好理解,就是看用户交互事件触发到页面响应中间耗时多少,如果其中有长任务发生的话那么势必会造成响应时间变长。

image.png

TBT

阻塞总时间,TBT(Total Blocking Time),记录在 FCP 到 TTI 之间所有长任务的阻塞时间总和。

假如说在 FCP 到 TTI 之间页面总共执行了以下长任务(执行时间大于 50ms)及短任务(执行时间低于 50ms)

那么每个长任务的阻塞时间就等于它所执行的总时间减去 50ms

所以对于上图的情况来说,TBT 总共等于 345ms。

这个指标的高低其实也影响了 TTI 的高低,或者说和长任务相关的几个指标都有关联性。

CLS

累计位移偏移,CLS(Cumulative Layout Shift),记录了页面上非预期的位移波动。

大家想必遇到过这类情况:页面渲染过程中突然插入一张巨大的图片或者说点击了某个按钮突然动态插入了一块内容等等相当影响用户体验的网站。这个指标就是为这种情况而生的,计算方式为:位移影响的面积 * 位移距离。

以上图为例,文本移动了 25% 的屏幕高度距离(位移距离),位移前后影响了 75% 的屏幕高度面积(位移影响的面积),那么 CLS 为 0.25 * 0.75 = 0.1875

CLS 推荐值为低于 0.1,越低说明页面跳来跳去的情况就越少,用户体验越好。毕竟很少有人喜欢阅读或者交互过程中网页突然动态插入 DOM 的情况,比如说插入广告~

介绍完了所有的指标,接下来我们来了解哪些是用户体验三大核心指标、如何获取相应的指标数据及如何优化。

三大核心指标

Google 在20年五月提出了网站用户体验的三大核心指标,分别为:

  • LCP 衡量加载体验
  • FID 衡量页面交互性
  • CLS 衡量视觉稳定性

如何获取指标

Lighthouse

你可以通过安装 Lighthouse 插件来获取如下指标

web-vitals 库

可以通过安装 web-vitals 包来获取如下指标

image.png

代码使用也很简单

import {getCLS, getFID, getLCP} from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);

3.性能瓶颈分析

想要对页面进行性能优化,除了了解关键性能指标之外,还需要了解页面加载全过程。通过这个过程,我们可以找到其中影响性能的瓶颈点。

页面加载大致过程是怎样的呢?

可以分为以下两个阶段:

  • 网络请求阶段
  • 页面渲染阶段

3.1网络请求阶段的瓶颈点

image.png

网络请求阶段,是指用户在浏览器输入 URL,经过本地缓存确认是否已经存在这个网站,如果没有,接着会由 DNS 查询从域名服务器获取这个 IP 地址,接下来就是客户端通过 TCP 的三次握手和TLS协商向服务器发起 HTTP 请求建立连接,然后服务端响应的过程。

在这个过程中,缓存、DNS查询、HTTP 请求和服务端响应很容易成为影响前端性能的瓶颈点。

3.1.1缓存

缓存可以让静态资源加载更快,当客户端发起一个请求时,静态资源可以直接从客户端中获取,不需要再向服务器请求。

image.png

缓存可以分为四类,它们按照获取资源时请求的优先级依次排列如下:

1.Memory Cache

MemoryCache,是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。

那么,哪些文件会被放入内存呢?

事实上,这个划分规则,一直以来是没有定论的。虽然划分规则没有定论,但根据日常开发中观察的结果,我们可以总结出这样的规律:资源存不存内存,浏览器秉承的是“节约原则”。像Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以为浏览器节省渲染开销。

2.Service Worker Cache

Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。

下面通过一个简单的例子来了解下Service Worker 如何为我们实现离线缓存。

// index.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js').then(function () {
        // 注册成功
    });
}

// sw.js
// Service Worker会监听所有的网络请求
self.addEventListener('fetch', function (e) {
    // 如果有cache则直接返回,否则通过fetch请求
    e.respondWith(
        caches.match(e.request).then(function (cache) {
            return cache || fetch(e.request);
        }).catch(function (err) {
            console.log(err);
            return fetch(e.request);
        })
    );
});

3.HTTP Cache

HTTP缓存一般包括强缓存和协商缓存两种形式。

强缓存是指浏览器在加载资源时,根据请求头的expirescache-control 判断是否命中客户端缓存。如果命中,则直接从缓存读取资源,不会发请求到服务器,反之还需要走完整的资源请求流程。

协商缓存是指,浏览器会先发送一个请求到服务器,通过 last-modified etag 验证资源是否命中客户端缓存。如果命中,服务器会将这个请求返回,但不会返回这个资源的数据,依然是从缓存中读取资源。 如果没有命中,无论是资源过期或者没有相关资源,都需要向服务器发起请求,等待服务器返回这个资源。

4.Push Cache

Push Cache 其实是 HTTP/2 的 Push 功能所带来的。简单来说,就是你在请求一个资源的时候,服务端可以为你“推送”一些其他资源 —— 你可能在在不久的将来就会用到一些资源。

3.1.2 DNS 查询

image.png

DNS 之所以会成为前端性能瓶颈点,是因为每进行一次 DNS 查询,都要经历从浏览器DNS缓存,操作系统DNS缓存,本地域名服务器,再到根域名服务器的过程。这中间需要很长的时间。

3.1.3 HTTP 请求

在 HTTP 请求阶段,最大的瓶颈点来源于请求阻塞。所谓请求阻塞,就是浏览器为保证访问速度,会默认对同一域下的资源保持一定的连接数,请求过多就会进行阻塞。

那么,浏览器同域名的连接数限制是多少呢?一般是 6 个。如果当前请求书多于 6 个,只能 6 个并发,其余的得等最先返回的请求后,才能做下一次请求。

3.1.4服务端响应

服务端响应阶段,是指 WebServer 接收到请求后,从数据存储层取到数据,再返回给前端的过程。

这个过程中的瓶颈点,就在于是否做了数据缓存、是否做了压缩,以及是否有重定向。

数据缓存

image.png

压缩

服务器端通过使用压缩,传输到浏览器端的文本类资源(有别于图片等二进制等资源)的大小可以变为原来的 1/3 左右。因此通过压缩,资源的下载速度会快很多,能大大提升页面的展示速度。

Gzip是常用的压缩算法,当然还有一种新的算法Brotli ,简称br,它比gzip提供更好的压缩。

image.png

重定向

所谓重定向,是指网站资源(如表单,整个站点等)迁移到其他位置后,用户访问站点时,程序自动将用户请求从一个页面转移到另外一个页面的过程。

重定向分为三类:服务端发挥的302重定向,META 标签实现的重定向和前端 Javasript 通过window.location 实现的重定向。

为什么说重定向也是一个瓶颈点,因为每做一次重定向,不仅增加了一次请求往返,还有可能导致新域名的 DNS 解析,而这些会导致请求过程中更多的时间。

3.2页面渲染阶段的瓶颈点

在页面加载过程中,当前服务端对数据加工聚合处理后,客户端拿到数据,接下来就会进入解析和渲染阶段。

所谓解析,就是 HTML 解析器把页面内容转换为 DOM 树和 CSSOM树的过程。

解析完后就是渲染。主线程会计算 DOM 节点的最终样式,生成布局树。布局树会记录参与页面布局的节点和样式。完成布局后,紧跟着就是绘制。绘制就是把各个节点绘制到屏幕上的过程,绘制结果以层的方式保存。当文档中各个部分以不同的层绘制时,相互重叠时,就必须进行合成,以确保他们可以以正确的顺序绘制和展示。

以上就是解析和渲染阶段,这个阶段流程环节多,逻辑复杂,瓶颈点也多,比如,DOM 树构建过程,CSSOM 树生成阶段,重排和重绘过程等。在这里我会重点介绍一下 DOM 树构建和布局两个环节的瓶颈点。

构建 DOM 树的瓶颈点

解析器构建 DOM 树的过程中,有三点会严重影响前端性能。

一个是当 HTML 标签不满足 Web 语义化时,浏览器就需要更多时间去解析 DOM 标签的含义。特别解析器是对标签的容错,比如将
写成了
,又或者表格嵌套不标准,标签层次结构复杂等。

另一个是 DOM 节点的数量越多,构建 DOM 树的时间就会变长,进而延长解析时间,拖慢页面展示速度。

最后一个是文档中包含

所以,外部脚本的加载时机一定要确定好,能够延迟加载就选用延迟加载。

布局中的瓶颈点

在布局阶段,浏览器会根据样式解析器给出的样式规则,来计算某个元素需要占据的空间大小和屏幕中的位置(比如电商详情页一张 banner图片的高度、宽度和位置),借助结算结果,来进行布局。而主线程布局时,使用的是流模型的布局方式。所谓流模型,就是像水流一样,需要从左到右,从上到下遍历一遍所有元素。

假设我们在页面渲染过程运行时修改了一个元素的属性,比如在电商的商品详情页加入一条广告数据。这时布局阶段受到了影响。浏览器必须检查所有其他区域的元素,然后自动重排页面,受到影响的元素需要重新绘制,最后还得合成,相当于整个渲染流程再来了一遍。

除此之外,因为浏览器每次布局计算都要作用于整个 DOM,如果元素量大,计算出所有元素的位置和尺寸会花很长的时间。所以布局阶段很容易成为性能瓶颈点。

4.网络请求优化

4.1静态资源缓存

如何做静态缓存方案呢?

  • 一种是静态资源长期不需要修改
  • 一种是静态资源修改频繁的

资源长期不变的话,比如 1 年都不怎么变化,我们可以使用强缓存,如 Cache-Control 来实现。具体来说可以通过设置 Cache-Control:max-age=31536000,来让浏览器在一年内直接使用本地缓存文件,而不是向服务端发出请求。

至于第二种,**如果资源本身随时会发生改动的,可以通过设置 Etag 实现协商缓存。**具体来说,在初次请求资源时,设置 Etag(比如使用资源的 md5 作为 Etag),并且返回 200 的状态码,之后请求时带上 If-none-match 字段,来询问服务器当前版本是否可用。如果服务端数据没有变化,会返回一个 304 的状态码给客户端,告诉客户端不需要请求数据,直接使用之前缓存的数据即可。

4.2 DNS 查询优化

如何对 DNS 查询进行优化呢?

通过在页面中加入 dns-prefetch,在静态资源请求之前对域名进行解析,从而减少用户进入页面的等待时间。如下所示:

<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="https://www.baidu.com/" >

其中第一行中的 x-dns-prefetch-control 表示开启 DNS 预解析功能,第二行 dns-prefetch 表示强制对百度的域名做预解析。这样在百度的资源请求开始前,DNS 解析完成,后续请求就不需要重复做解析了。

4.3 HTTP请求优化

4.3.1减少请求数

懒加载

懒加载是指在长页面加载过程时,先加载关键内容,延迟加载非关键内容。

  • 路由懒加载
  • 图片懒加载

文件合并

  • 第三方依赖
  • 雪碧图
  • base64图片

4.3.2减少单次请求所花费的时间

文件压缩

压缩文件可以减少文件下载时间,让用户体验性更好。

在开发阶段我们可以使用构建工具对文件进行压缩,在服务器响应阶段可以使用Gzip或Brotli压缩,可以通过向 HTTP 请求头中的 accept-encoding: gzip, br告知服务器可接受的压缩算法。

Tree Shaking

本质是通过检测源码中不会被使用到的部分,将其删除,从而减小代码的体积。

使用CDN

CDN通过在网络各处放置节点服务器,构造一个智能虚拟网络,将用户的请求导向离用户最近的服务节点上。

CDN 的核心点有两个,一个是缓存,一个是回源

“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。

CDN 的优化细节:我们知道,同一个域名下的请求会携带 Cookie,而静态资源往往并不需要 Cookie 携带什么认证信息。把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现!

4.3.3增加请求并行连接数

域名分片

通过不同的域名,增加请求并行连接数。

常见做法是,将静态服务器地址 pic.google.com,做成支持 pic0-5 的 6 个域名,每次请求时随机选一个域名地址进行请求。因为有 6 个域名同时可用,最多可以并行 36 个连接。

HTTP2

HTTP 2.0 提供了多路复用的功能,传输数据采用二进制数据帧和流的方式进行传输。

使用HTTP2后,单个文件可以单独上线,不需要做合并。

http2.akamai.com/demo

5.页面解析和渲染优化

5.1 CSS 优化

在给出 CSS 选择器方面的优化建议之前,先告诉大家一个小知识:CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配。 看如下规则:

#myList li {}

这样的写法其实很常见。大家平时习惯了从左到右阅读的文字阅读方式,会本能地以为浏览器也是从左到右匹配 CSS 选择器的,因此会推测这个选择器并不会费多少力气:#myList 是一个 id 选择器,它对应的元素只有一个,查找起来应该很快。定位到了 myList 元素,等于是缩小了范围后再去查找它后代中的 li 元素,没毛病。

事实上,CSS 选择符是从右到左进行匹配的。我们这个看似“没毛病”的选择器,实际开销相当高:浏览器必须遍历页面上每个 li 元素,并且每次都要去确认这个 li 元素的父元素 id 是不是 myList,你说坑不坑!

这样一看,一个小小的 CSS 选择器,也有不少的门道!好的 CSS 选择器书写习惯,可以为我们带来非常可观的性能提升。根据上面的分析,我们至少可以总结出如下性能提升的方案:

  • 避免使用通配符,只对需要用到的元素进行选择。
  • 少用标签选择器。尽量用类选择器替代。
  • 减少嵌套(最高不要超过三层)

5.2告别阻塞

CSS阻塞:默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容

JS阻塞:当 DOM 解析遇到 JavaScript 脚本时,会停止解析,开始下载脚本并执行,再恢复解析,相当于是阻塞了 DOM 构建。

所以我们现在都习惯把 CSS 样式表放在 <head> 之中(即页面的头部),把 JavaScript 脚本放在 <body> 的最后。

当然有时我们想把JavaScript 脚本放在中,但又不想阻塞DOM构建,这时可以使用 deferasync 属性。两者都会防止 JavaScript 脚本的下载阻塞 DOM 构建。但是两者也有区别,最直观的表现如下:

defer 会在 HTML 解析完成后,按照脚本出现的次序再顺序执行;而 async 则是下载完成就立即开始执行,同时阻塞页面解析,不保证脚本间的执行顺序。

5.3回流与重绘

哪些实际操作会导致回流与重绘

要避免回流与重绘的发生,最直接的做法是避免掉可能会引发回流与重绘的 DOM 操作,就好像拆弹专家在解决一颗炸弹时,最重要的是掐灭它的导火索。

触发重绘的“导火索”比较好识别——只要是不触发回流,但又触发了样式改变的 DOM 操作,都会引起重绘,比如背景色、文字色、可见性(可见性这里特指形如visibility: hidden这样不改变元素位置和存在性的、单纯针对可见性的操作,注意与display:none进行区分)等。为此,我们要着重理解一下那些可能触发回流的操作。

回流的导火索

  • 最“贵”的操作:改变 DOM 元素的几何属性

当一个DOM元素的几何属性发生变化时,所有和它相关的节点(比如父子节点、兄弟节点等)的几何属性都需要进行重新计算,它会带来巨大的计算量。

  • “价格适中”的操作:改变 DOM 树的结构

这里主要指的是节点的增减、移动等操作。浏览器引擎布局的过程,顺序上可以类比于树的前序遍历——它是一个从上到下、从左到右的过程。通常在这个过程中,当前元素不会再影响其前面已经遍历过的元素。

  • 最容易被忽略的操作:获取一些需要通过即时计算得到的属性,比如offsetTop,scrollTop等。

当你要获取一些需要通过即时计算得到的属性,浏览器为了获取这些值,也会进行回流。

如何规避回流与重绘

  • 将“导火索”缓存起来,避免频繁改动

有时我们想要通过多次计算得到一个元素的布局位置,我们可能会这样做:

<script>
  // 获取el元素
  const el = document.getElementById('el')
  // 这里循环判定比较简单,实际中或许会拓展出比较复杂的判定需求
  for(let i=0;i<10;i++) {
      el.style.top  = el.offsetTop  + 10 + "px";
      el.style.left = el.offsetLeft + 10 + "px";
  }
 </script>

每次循环都需要获取多次“敏感属性”,是比较糟糕的。我们可以将其以 JS 变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求

// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el') 
let offLeft = el.offsetLeft, offTop = el.offsetTop

// 在JS层面进行计算
for(let i=0;i<10;i++) {
  offLeft += 10
  offTop  += 10
}

// 一次性将计算结果应用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop  + "px"
  • 避免逐条改变样式,使用类名去合并样式
  • 将 DOM “离线”
let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了许多类似的后续操作)
container.style.display = 'block'

6.项目中的优化

6.1虚拟列表

虚拟列表是一种用来优化长列表的技术。它可以保证在列表元素很多的情况下,依然拥有很好的滚动、浏览性能。

它的核心思想在于:只渲染可见区域附近的列表元素。

大致的实现思路如下:

  1. 监听页面滚动(或者其他导致视口变化的事件);
  2. 滚动时根据滚动的距离计算需要展示的列表项;
  3. 将列表项中展示的数据与组件替换成当前需要展示的内容;
  4. 修改偏移量到对应的位置。

在开发中,我们一般会使用第三方库来实现。比如react的 react-window

使用 react-window 很简单,只需要计算每项的高度即可。下面代码中每一项的高度是 35px。

import { FixedSizeList as List } from "react-window"
const Row = ({ index, style }) => <div style={style}>Row {index}</div>

const Example = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35} // 每项的高度为 35
    width={300}
  >
    {Row}
  </List>
)

如果每项的高度是变化的,可给 itemSize 参数传一个函数。

6.2节流和防抖

debounce适合在搜索场景使用,秩序响应用户最后一次输入。

throttle 更适合在需要实时响应用户的场景使用,如通过拖拽调整尺寸或通过拖拽进行放大缩小。

下面是一个使用use-debounce来进行搜索的例子:

import { useState, useEffect } from "react"
import { useDebounce } from "use-debounce"

export default function App() {

  const [text, setText] = useState("Hello")
  const [debouncedValue] = useDebounce(text, 300)

  useEffect(() => {
    // 根据 debouncedValue 进行搜索
  }, [debouncedValue])
  return (
    <div>
      <input
        defaultValue={"Hello"}
        onChange={e => {
          setText(e.target.value)
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {debouncedValue}</p>
    </div>
  )
}

6.3 requestIdleCallback和 Web Worker

在上传文件场景中,如果要支持断点续传的功能,就需要计算文件的md5,来判断这个文件是否上传过。而计算文件md5是比较耗时的。

这时,可以通过两种方式来计算。一种是延迟计算,一种是并行计算。

延迟计算

利用requestIdleCallback 这个方法把计算放到后续的事件循环或空闲时刻。

async calculateHashIdle(chunks) {
    return new Promise(resolve => {
        let count = 0;
        const spark = new sparkMD5.ArrayBuffer();
        const appendToSpark = file => {
            return new Promise(resolve => {
                const fileReader = new FileReader();
                fileReader.onload = () => {
                    spark.append(fileReader.result);
                    resolve();
                };
                fileReader.readAsArrayBuffer(file);
            });
        };
        const workLoop = async deadling => {
            // 如果chunks没执行完并且有空闲时间
            while (count < chunks.length && deadling.timeRemaining() > 1) {
                // 把chunk加入spark
                await appendToSpark(chunks[count].chunk);
                count++;
                if (count < chunks.length) {
                    // 更新处理进度
                    this.hashProgress = Number(
                        ((count * 100) / chunks.length).toFixed(2)
                    );
                } else {
                    this.hashProgress = 100;
                    // 返回处理后的md5值
                    resolve(spark.end());
                }
            }
            window.requestIdleCallback(workLoop);
        };

        window.requestIdleCallback(workLoop);
    });
}

并行计算

使用Web Worker,可以并行地执行 JavaScript 。

// hash.js
self.importScripts("./spark-md5.min.js");

self.onmessage = e => {
    const { chunks } = e.data;
    const spark = new SparkMD5.ArrayBuffer();
    let len = chunks.length;

    const loadNext = cur => {
        const fileReader = new FileReader();
        fileReader.readAsArrayBuffer(chunks[cur].chunk);
        fileReader.onload = () => {
            spark.append(fileReader.result);
            cur++;
            if (cur < len) {
                const progress = Number(((cur * 100) / chunks.length).toFixed(2));
                self.postMessage({ progress });
                loadNext(cur);
            } else {
                self.postMessage({ progress: 100, hash: spark.end() });
            }
        };
    };

    loadNext(0);
};

// index.js
async calculateHashWorker(chunks) {
    return new Promise(resolve => {
        const worker = new Worker("hash.js");
        worker.postMessage({ chunks });
        worker.onmessage = e => {
            const { progress, hash } = e.data;
            if (!hash) {
                this.hashProgress = progress;
            } else {
                this.hashProgress = 100;
                resolve(hash);
            }
        };
    });
}

7.性能分析

最后我写了一个简单的卡顿的例子,我们尝试通过 Performance 来分析出这个例子中哪一行代码卡。首先在这个页面的 input 框中输入的时候,你能明显感觉到非常卡顿。

从上面的动图可以看到,最后上面一栏出现很多红线,这就代表性能出问题了。

我们知道 JS 是单线程的,也就是执行代码与绘制是同一个线程,必须等代码执行完,才能开始绘制。那具体是那一块代码执行时间长了呢?这里我们就要看 Main 这一栏,这一栏列出了 JS 调用栈。

image.png

image.png

点击Bottom-Up面板,按SelfTime进行排序,你可以看 onChange 函数执行了很长时间,点击它就能定位到具体的代码。

image.png

这是一个最简单的例子,这种由单个地方引起的性能问题,也是比较好解决的。

参考链接

性能指标

前端性能优化方法与实战

React 性能优化

前端性能优化原理与实践

React 项目性能分析及优化

performance

RUM指标

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿