# 前端性能优化

在前端领域,性能问题常常同样表现为空间和时间两种,顾名思义:

  • 空间性能问题可同样理解为内存占用、CPU 占用、本地缓存占用过多带来的问题(如卡顿);

  • 时间性能问题则意味着用户等待时间过长,包括页面加载、渲染、可交互等耗时。

# 浏览器显示页面的原理

浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎没有十分明确区分,但随着 JS 引擎越来越独立,内核成了渲染引擎的代称。

基本流程:

  • 获取 HTML 文档及样式表文件
  • 解析成对应的树形数据结构 1.DOM tree 2.CSSOM tree
  • 计算可见节点形成 render tree
  • 计算 DOM 的形状及位置进行布局
  • 将每个节点转化为实际像素绘制到视口上(栅格化) render tree(页面显示最终结果)是由 DOM tree与 CSSOM tree 合并并剔除不可见的节点所形成的,其中不包含如下节点:

1.本身不可见的

  • html
  • head
  • meta
  • link
  • style
  • script

2.设置了 display: none 样式的

注意

一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。

资源加载:

提示

执行 JavaScript 会阻止 DOM tree 构建 加载 CSS 会阻止 render tree 构建

影响性能的因素:

  • 白屏
  • HTML 和 CSS 的加载及解析速度
  • head 内的脚本加载及执行
  • 首屏
  • 图片加载
  • body 内的脚本加载及执行
  • render tree 的构建
  • HTML 的复杂度
  • CSS 的复杂度
  • render tree 的绘制(栅格化)
  • 颜色的复杂度
  • 形状的复杂度

# 优化选择器

CSS 选择符是从右到左进行匹配的

#myList li{} /*× 浏览器必须遍历页面上每个 li 元素,并且每次都要去确认这个 li 元素的父元素 id 是不是 myList*/
.myList_li {}/*√*/

.myList#title/*×不要画蛇添足,id 和 class 选择器不应该被多余的标签选择器拖后腿*/
#title/*√*/

# 减少 HTTP 请求数量

精灵图弃用 (opens new window) 浏览器与http创建TCP链接 (opens new window)

解析

基本原理:浏览器与服务器需要经过三次握手,每次握手需要花费大量时间。而且不同浏览器对资源文件并发请求数量有限(不同浏览器允许并发数),一旦 HTTP 请求数量达到一定数量,资源请求就存在等待状态,这是很致命的,因此减少 HTTP 的请求数量可以很大程度上对网站性能进行优化。

措施

1.CSS Sprites:CSS 精灵

2.合并 CSS 和 JS 文件。

3.采用 lazyLoad:俗称懒加载,比如滑动到页面相应高度再加载图片。

每一个 HTTP 请求在发送出去之前,都会经历一次 TCP 三次握手的过程,当数据传输完毕,又进行一次四次挥手的过程,是不是贼麻烦?

严格意义上来说,合并文件这种性能优化手段,在 HTTP 1.1 推出后,就应该受到质疑了。HTTP 1.1 版本默认支持长连接(Connection:keep-alive)和流水线请求(Pipelining),也就是说,多个 HTTP 请求可以依次共享同一个 TCP 连接,大大减少了建立 TCP 连接时的消耗和延迟。

2015 年,HTTP 2.0 横空出世,带着多路复用的光环来了,不仅解决了 HTTP 1.1 流水线请求会造成的请求阻塞的问题,更重要的是,多路并行,更快了!

在现代前端工程里,没有必要做这种「合并文件以减少 HTTP 请求」的“优化”,相反的,现在的前端应用都很大,反而需要用到「分包加载」和「懒加载」等手段,减少每个静态文件的实际体积,这样才能获得更快的下载速度以及在浏览器端获得更快的解析速度。

Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。

# 压缩js,css,html,图片 避免使用CSS表达式

webpack等工具,图片根据需求寻求jpg或者svg,base64,png;代码按需加载,还可以代码减重(tree shaking)

# 后台在传输途径压缩

前端压缩的文件,其实再下载的时候,后台返回时也可以继续压缩,原本已经压缩的dist.min.js有大约46kb[资源大小],后台如node使用compression继续压缩后仅仅只有17kb(本地测试把文件达成zip包也是差不多17kb[network传输])

304有缓存页面加载的那么快,传输所占用的体积已经是非常小了,但是图片不建议gzip缓存。首先,HTTP压缩需要成本。Web服务器获得需要的内容,然后压缩它,最后将它发送到客户端。如果内容不能被进一步压缩,你只是在浪费CPU做无意义的任务。其次,采用HTTP压缩已经被过压缩的东西并不能使它更小。事实上,添加标头,压缩字典,并校验响应体实际上使它变得更大

# 避免页面中空的 href 和 src

解析

避免使用空的 src 属性确实可以缩减浏览器首屏渲染的时间,因为浏览器在渲染过程中会把 src 属性中的空内容进行加载,直至加载失败,影响 DOMContentLoaded 与 Loaded 事件之间的资源准备过程,拉长了首屏渲染所用的时间;但空的 href 属性对首屏渲染的影响比较小。

# 控制资源文件加载优先级

基本原理:说到这里就需要知道浏览器加载 HTML 内容的原理,浏览器在加载 HTML 内容时,是将 HTML 内容从上至下依次解析,解析到 link 或者 script 标签就会加载 href 或者 src 对应链接内容,为了第一时间展示页面给用户,就需要将 CSS 提前加载,不要受 JS 加载影响。

# 利用浏览器缓存

基本原理:浏览器缓存分强缓存和协商缓存,他们是将网络资源存储在本地,等待下次请求该资源时,如果命中就不需要到服务器重新请求该资源,直接在本地读取该资源。

类型

  • 强缓存:在 web 服务器返回的响应中添加 Expires 和 Cache-Control Header。
  • 协商缓存:通过【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对 Header 分别管理。
  • 本地缓存 cookie localstorage indexdb
  • 强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码;
  • 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;
  • 两者的共同点是,都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求

# 搞懂前端缓存

总的来说:

  • 如果开启了Service Worker首先会从Service Worker中拿
  • 如果新开一个以前打开过的页面缓存会从Disk Cache中拿(前提是命中强缓存)
  • 刷新当前页面时浏览器会根据当前运行环境内存来决定是从 Memory Cache 还是 从Disk Cache中拿(可以看到下图最后几个文件有时候是从 Memory Cache中拿有时候是从Disk Cache中拿)

# 前端缓存/后端缓存

基本的网络请求就是三个步骤:请求,处理,响应。

后端缓存主要集中于“处理”步骤,通过保留数据库连接,存储处理结果等方式缩短处理时间,尽快进入“响应”步骤。

而前端缓存则可以在剩下的两步:“请求”和“响应”中进行。在“请求”步骤中,浏览器也可以通过存储结果的方式直接使用资源,直接省去了发送请求;而“响应”步骤需要浏览器和服务器共同配合,通过减少响应内容来缩短传输时间

  • 按缓存位置分类 (memory cache, disk cache, Service Worker 等)
  • 按失效策略分类 (Cache-Control, ETag 等)
  • 缓存的应用模式
  1. 按缓存位置分类

可以在 Chrome 的开发者工具中,Network -> Size 一列看到一个请求最终的处理方式:如果是大小 (多少 K, 多少 M 等) 就表示是网络请求,否则会列出 from memory cache, from disk cache 和 from ServiceWorker。

它们的优先级是:(由上到下寻找,找到即返回;找不到则继续)

  • Service Worker
  • Memory Cache
  • Disk Cache
  • 网络请求

  • memory cache

memory cache 是内存中的缓存,(与之相对 disk cache 就是硬盘上的缓存)。按照操作系统的常理:先读内存,再读硬盘。 (disk cache因为它的优先级更低一些)e。

几乎所有的网络请求资源都会被浏览器自动加入到 memory cache 中。但是也正因为数量很大但是浏览器占用的内存不能无限扩大这样两个因素,memory cache 注定只能是个“短期存储”。 常规情况下,浏览器的 TAB 关闭后该次浏览的 memory cache 便告失效 (为了给其他 TAB 腾出位置)。而如果极端情况下 (例如一个页面的缓存就占用了超级多的内存),那可能在 TAB 没关闭之前,排在前面的缓存就已经失效了。

刚才提过,几乎所有的请求资源 都能进入 memory cache,这里细分一下主要有两块:

  • preloader

在浏览器打开网页的过程中,会先请求 HTML 然后解析。之后如果浏览器发现了 js, css 等需要解析和执行的资源时,它会使用 CPU 资源对它们进行解析和执行。在古老的年代(大约 2007 年以前),“请求 js/css - 解析执行 - 请求下一个 js/css - 解析执行下一个 js/css” 这样的“串行”操作模式在每次打开页面之前进行着。很明显在解析执行的时候,网络请求是空闲的,这就有了发挥的空间:能不能一边解析执行 js/css,一边去请求下一个(或下一批)资源呢?

这就是 preloader 要做的事情。不过 preloader 没有一个官方标准,所以每个浏览器的处理都略有区别。例如有些浏览器还会下载 css 中的 @import 内容 poster等。

而这些被 preloader 请求够来的资源就会被放入 memory cache 中,供之后的解析执行操作使用。

  • preload(虽然看上去和刚才的 preloader 就差了俩字母)。实际上这个大家应该更加熟悉一些。这些显式指定的预加载资源,也会被放入 memory cache 中

memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的 ,两个href相同的)都实际只会被请求最多一次,避免浪费。

不过在匹配缓存时,除了匹配完全相同的 URL 之外,还会比对他们的类型,CORS 中的域名规则等。因此一个作为脚本 (script) 类型被缓存的资源是不能用在图片 (image) 类型的请求中的,即便他们 src 相等。

在从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置。例如页面上存在几个相同 src 的图片,即便它们可能被设置为不缓存,但依然会从 memory cache 中读取。这是因为 memory cache 只是短期使用,大部分情况生命周期只有一次浏览而已。而 max-age=0 在语义上普遍被解读为“不要在下次浏览时使用”,所以和 memory cache 并不冲突。

但如果站长是真心不想让一个资源进入缓存,就连短期也不行,那就需要使用 no-store。存在这个头部配置的话,即便是 memory cache 也不会存储,自然也不会从中读取了。(后面的第二个示例有关于这点的体现)

  • disk cache

disk cache 也叫 HTTP cache ,顾名思义是存储在硬盘上的缓存,因此它是持久存储的,是实际存在于文件系统中的。而且它允许相同的资源在跨会话,甚至跨站点的情况下使用,例如两个站点都使用了同一张图片。

disk cache 会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存;哪些资源是仍然可用的,哪些资源是过时需要重新请求的。当命中缓存之后,浏览器会从硬盘中读取资源,虽然比起从内存中读取慢了一些,但比起网络请求还是快了不少的。绝大部分的缓存都来自 disk cache。

关于 HTTP 的协议头中的缓存字段,我们会在稍后进行详细讨论。

凡是持久性存储都会面临容量增长的问题,disk cache 也不例外。在浏览器自动清理时,会有神秘的算法去把“最老的”或者“最可能过时的”资源删除,因此是一个一个删除的。不过每个浏览器识别“最老的”和“最可能过时的”资源的算法不尽相同,可能也是它们差异性的体现。

  • Service Worker

上述的缓存策略以及缓存/读取/失效的动作都是由浏览器内部判断 & 进行的,我们只能设置响应头的某些字段来告诉浏览器,而不能自己操作。举个生活中去银行存/取钱的例子来说,你只能告诉银行职员,我要存/取多少钱,然后把由他们会经过一系列的记录和手续之后,把钱放到金库中去,或者从金库中取出钱来交给你。

但 Service Worker 的出现,给予了一种更加灵活,更加直接的操作方式。依然以存/取钱为例,我们现在可以绕开银行职员,自己走到金库前(当然是有别于上述金库的一个单独的小金库),自己把钱放进去或者取出来。因此我们可以选择放哪些钱(缓存哪些文件),什么情况把钱取出来(路由匹配规则),取哪些钱出来(缓存匹配并返回)。

Service Worker 能够操作的缓存是有别于浏览器内部的 memory cache 或者 disk cache 的。可以从 Chrome 的 F12 中,Application -> Cache Storage找到这个单独的“小金库”。除了位置不同之外,这个缓存是永久性的,即关闭 TAB 或者浏览器,下次打开依然还在(而 memory cache 不是)。有两种情况会导致这个缓存中的资源被清除:手动调用 API cache.delete(resource) 或者容量超过限制,被浏览器全部清空。

如果 Service Worker 没能命中缓存,一般情况会使用 fetch() 方法继续获取资源。这时候,浏览器就去 memory cache 或者 disk cache 进行下一次找缓存的工作了。注意:经过 Service Worker 的 fetch() 方法获取的资源,即便它并没有命中 Service Worker 缓存,甚至实际走了网络请求,也会标注为 from ServiceWorker。

  • 请求网络

如果一个请求在上述 3 个位置都没有找到缓存,那么浏览器会正式发送网络请求去获取内容。之后容易想到,为了提升之后请求的缓存命中率,自然要把这个资源添加到缓存中去。具体来说:

  • 根据 Service Worker 中的 handler 决定是否存入 Cache Storage (额外的缓存位置)。
  • 根据 HTTP 头部的相关字段(Cache-control, Pragma 等)决定是否存入 disk cache
  • memory cache 保存一份资源 的引用,以备下次使用。
  1. 按失效策略分类

memory cache 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束,算是一个黑盒。Service Worker 是由开发者编写的额外的脚本,且缓存位置独立,出现也较晚,使用还不算太广泛。所以平时最为熟悉的其实是 disk cache,也叫 HTTP cache (因为不像 memory cache,它遵守 HTTP 协议头中的字段)。平时所说的强制缓存,对比缓存,以及 Cache-Control 等,也都归于此类。

I 强制缓存 (也叫强缓存)

强制缓存的含义是,当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回;不存在则请求真的服务器,响应后再写入缓存数据库。

强制缓存直接减少请求数,是提升最大的缓存策略。 它的优化覆盖了文章开头提到过的请求数据的全部三个步骤。如果考虑使用缓存来优化网页性能的话,强制缓存应该是首先被考虑的。

可以造成强制缓存的字段是 Cache-control 和 Expires。

  • Expires 这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间),如
Expires: Thu, 10 Nov 2017 08:45:11 GMT

在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。

但是,这个字段设置时有两个缺点:

①由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑自信修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。

②写法太复杂了。表示时间的字符串多个空格,少个字母,都会导致非法属性从而设置失效。

  • Cache-control 已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求

这两者的区别就是前者是绝对时间,而后者是相对时间。如下:

Cache-control: max-age=2592000

下面列举一些 Cache-control 字段常用的值:(完整的列表可以查看 MDN)

  • max-age:即最大有效时间
  • must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
  • no-cache:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。
  • no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
  • public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
  • private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。

这些值可以混合使用,例如 Cache-control:public, max-age=2592000。在混合使用时,它们的优先级如下图.

这里有一个疑问:max-age=0 和 no-cache 等价吗?从规范的字面意思来说,max-age 到期是 应该重新验证,而 no-cache 是 必须重新验证。但实际情况以浏览器实现为准,大部分情况他们俩的行为还是一致的。(如果是 max-age=0, must-revalidate 就和 no-cache 等价了)

顺带一提,在 HTTP/1.1 之前,如果想使用 no-cache,通常是使用 Pragma 字段,如 Pragma: no-cache(这也是 Pragma 字段唯一的取值)。但是这个字段只是浏览器约定俗成的实现,并没有确切规范,因此缺乏可靠性。它应该只作为一个兼容字段出现,在当前的网络环境下其实用处已经很小。

总结一下,自从 HTTP/1.1 开始,Expires 逐渐被 Cache-control 取代。Cache-control 是一个相对时间,即使客户端时间发生改变,相对时间也不会随之改变,这样可以保持服务器和客户端的时间一致性。而且 Cache-control 的可配置性比较强大。

Cache-control 的优先级高于 Expires,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段我们都会设置。

II 对比缓存 (也叫协商缓存)

当强制缓存失效(超过规定时间)时,就需要使用对比缓存,由服务器决定缓存内容是否失效。

流程上说,浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿这个标识和服务器通讯。如果缓存未失效,则返回 HTTP 状态码 304 表示继续使用,于是客户端继续使用缓存;如果失效,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库。

对比缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。它的优化覆盖了文章开头提到过的请求数据的三个步骤中的最后一个:“响应”。通过减少响应体体积,来缩短网络传输时间。所以和强制缓存相比提升幅度较小,但总比没有缓存好。

对比缓存是可以和强制缓存一起使用的,作为在强制缓存失效后的一种后备方案。实际项目中他们也的确经常一同出现。

对比缓存有 2 组字段(不是两个):

  • Last-Modified & If-Modified-Since
  1. 服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如
Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  1. 浏览器将这个值和内容一起记录在缓存数据库中。

  2. 下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段

  3. 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

但是他还是有一定缺陷的:

①如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。

②如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。

  • Etag & If-None-Match

为了解决上述问题,出现了一组新的字段 Etag 和 If-None-Match

Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。

Etag 的优先级高于 Last-Modified

# 缓存小结

当浏览器要请求资源时

  • 调用 Service Worker 的 fetch 事件响应
  • 查看 memory cache
  • 查看 disk cache。这里又细分:
    • 如果有强制缓存且未失效,则使用强制缓存,不请求服务器。这时的状态码全部是 200
    • 如果有强制缓存但已失效,使用对比缓存,比较后确定 304 还是 200
  • 发送网络请求,等待网络响应
    • 把响应内容存入 disk cache (如果 HTTP 头信息配置可以存的话)
    • 把响应内容 的引用 存入 memory cache (无视 HTTP 头信息的配置)
    • 把响应内容存入 Service Worker 的 Cache Storage (如果 Service Worker 的脚本调用了 cache.put())

# 缓存实例

  • memory cache & disk cache

写一个简单的 index.html,然后引用 3 种资源,分别是 index.js, index.css 和 mashroom.jpg。

给这三种资源都设置上 Cache-control: max-age=86400,表示强制缓存 24 小时。以下截图全部使用 Chrome 的隐身模式。

  1. 首次请求
  1. 再次请求 (F5) 第二次请求,三个请求都来自 memory cache。因为没有关闭 TAB,所以浏览器把缓存的应用加到了 memory cache。(耗时 0ms,也就是 1ms 以内)
  1. 关闭 TAB,打开新 TAB 并再次请求

因为关闭了 TAB,memory cache 也随之清空。但是 disk cache 是持久的,于是所有资源来自 disk cache。(大约耗时 3ms,因为文件有点小)

而且对比 2 和 3,很明显看到 memory cache 还是比 disk cache 快得多的。

  • no-cache & no-store index.html 里面一些代码,完成两个目标:

每种资源都(同步)请求两次;增加脚本异步请求图片

<!-- 把3种资源都改成请求两次 -->
<link rel="stylesheet" href="/static/index.css">
<link rel="stylesheet" href="/static/index.css">
<script src="/static/index.js"></script>
<script src="/static/index.js"></script>
<img src="/static/mashroom.jpg">
<img src="/static/mashroom.jpg">

<!-- 异步请求图片 -->
<script>
    setTimeout(function () {
        let img = document.createElement('img')
        img.src = '/static/mashroom.jpg'
        document.body.appendChild(img)
    }, 1000)
</script>

当把服务器响应设置为 Cache-Control: no-cache 时,我们发现打开页面之后,三种资源都只被请求 1 次。

这说明两个问题:

①同步请求方面,浏览器会自动把当次 HTML 中的资源存入到缓存 (memory cache),这样碰到相同 src 的图片就会自动读取缓存(但不会在 Network 中显示出来)

②异步请求方面,浏览器同样是不发请求而直接读取缓存返回。但同样不会在 Network 中显示。

总体来说,如上面原理所述,no-cache 从语义上表示下次请求不要直接使用缓存而需要比对,并不对本次请求进行限制。因此浏览器在处理当前页面时,可以放心使用缓存。

当把服务器响应设置为 Cache-Control: no-store 时,情况发生了变化,三种资源都被请求了 2 次。而图片因为还多一次异步请求,总计 3 次。(红框中的都是那一次异步请求)

这同样说明:

① 如之前原理所述,虽然 memory cache 是无视 HTTP 头信息的,但是 no-store 是特别的。在这个设置下,memory cache 也不得不每次都请求资源。

② 异步请求和同步遵循相同的规则,在 no-store 情况下,依然是每次都发送请求,不进行任何缓存。

  • Service Worker & memory (disk) cache

尝试把 Service Worker 也加入进去。我们编写一个 serviceWorker.js,并编写如下内容:(主要是预缓存 3 个资源,并在实际请求时匹配缓存并返回)

// serviceWorker.js
self.addEventListener('install', e => {
  // 当确定要访问某些资源时,提前请求并添加到缓存中。
  // 这个模式叫做“预缓存”
  e.waitUntil(
    caches.open('service-worker-test-precache').then(cache => {
      return cache.addAll(['/static/index.js', '/static/index.css', '/static/mashroom.jpg'])
    })
  )
})

self.addEventListener('fetch', e => {
  // 缓存中能找到就返回,找不到就网络请求,之后再写入缓存并返回。
  // 这个称为 CacheFirst 的缓存策略。
  return e.respondWith(
    caches.open('service-worker-test-precache').then(cache => {
      return cache.match(e.request).then(matchedResponse => {
        return matchedResponse || fetch(e.request).then(fetchedResponse => {
          cache.put(e.request, fetchedResponse.clone())
          return fetchedResponse
        })
      })
    })
  )
})

注册 SW 的代码这里就不赘述了。此外还给服务器设置 Cache-Control: max-age=86400 来开启 disk cache。看看两者的优先级。

①当首次访问时,会看到常规请求之外,浏览器(确切地说是 Service Worker)额外发出了 3 个请求。这来自预缓存的代码。

②第二次访问(无论关闭 TAB 重新打开,还是直接按 F5 刷新)都能看到所有的请求标记为 from SerciceWorker。

③from ServiceWorker 只表示请求通过了 Service Worker,至于到底是命中了缓存,还是继续 fetch() 方法光看这一条记录其实无从知晓。因此还得配合后续的 Network 记录来看。因为之后没有额外的请求了,因此判定是命中了缓存。

从服务器的日志也能很明显地看到,3 个资源都没有被重新请求,即命中了 Service Worker 内部的缓存。

如果修改 serviceWorker.js 的 fetch 事件监听代码,改为如下:

// 这个也叫做 NetworkOnly 的缓存策略。
self.addEventListener('fetch', e => {
  return e.respondWith(fetch(e.request))
})

可以发现在后续访问时的效果和修改前是 完全一致的。(即 Network 仅有标记为 from ServiceWorker 的几个请求,而服务器也不打印 3 个资源的访问日志)

很明显 Service Worker 这层并没有去读取自己的缓存,而是直接使用 fetch() 进行请求。所以此时其实是 Cache-Control: max-age=86400 的设置起了作用,也就是 memory/disk cache。但具体是 memory 还是 disk 这个只有浏览器自己知道了,因为它并没有显式的告诉我们。

# 浏览器的行为

所谓浏览器的行为,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache)。服务器直接返回 200 和最新内容。
# 缓存的应用模式

模式 1:不常变化的资源

Cache-Control: max-age=31536000

通常在处理这类资源资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,达到更改引用 URL 的目的,从而让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。

在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。如果配置中还增加 public 的话,CDN 也可以缓存起来,效果拔群。

这个模式的一个变体是在引用 URL 后面添加参数 (例如 ?v=xxx 或者 ?_=xxx),这样就不必在文件名或者路径中包含动态参数,满足某些完美主义者的喜好。在项目每次构建时,更新额外的参数 (例如设置为构建时的当前时间),则能保证每次构建后总能让浏览器请求最新的内容。

特别注意: 在处理 Service Worker 时,对待 sw-register.js(注册 Service Worker) 和 serviceWorker.js (Service Worker 本身) 需要格外的谨慎。如果这两个文件也使用这种模式,你必须多多考虑日后可能的更新及对策。

模式 2:经常变化的资源

Cache-Control: no-cache

这里的资源不单单指静态资源,也可能是网页资源,例如博客文章。这类资源的特点是:URL 不能变化,但内容可以(且经常)变化。我们可以设置 Cache-Control: no-cache 来迫使浏览器每次请求都必须找服务器验证资源是否有效。

既然提到了验证,就必须 ETag 或者 Last-Modified 出场。这些字段都会由专门处理静态资源的常用类库(例如 koa-static)自动添加,无需开发者过多关心。

也正如上文中提到协商缓存那样,这种模式下,节省的并不是请求数,而是请求体的大小。所以它的优化效果不如模式 1 来的显著。

模式 3:非常危险的模式 1 和 2 的结合 (反例)

Cache-Control: max-age=600, must-revalidate

不知道是否有开发者从模式 1 和 2 获得一些启发:模式 2 中,设置了 no-cache,相当于 max-age=0, must-revalidate。我的应用时效性没有那么强,但又不想做过于长久的强制缓存,我能不能配置例如 max-age=600, must-revalidate 这样折中的设置呢?

表面上看这很美好:资源可以缓存 10 分钟,10 分钟内读取缓存,10 分钟后和服务器进行一次验证,集两种模式之大成,但实际线上暗存风险。因为上面提过,浏览器的缓存有自动清理机制,开发者并不能控制。

举个例子:当有 3 种资源: index.html, index.js, index.css。我们对这 3 者进行上述配置之后,假设在某次访问时,index.js 已经被缓存清理而不存在,但 index.html, index.css 仍然存在于缓存中。这时候浏览器会向服务器请求新的 index.js,然后配上老的 index.html, index.css 展现给用户。这其中的风险显而易见:不同版本的资源组合在一起,报错是极有可能的结局。

除了自动清理引发问题,不同资源的请求时间不同也能导致问题。例如 A 页面请求的是 A.js 和 all.css,而 B 页面是 B.js 和 all.css。如果我们以 A -> B 的顺序访问页面,势必导致 all.css 的缓存时间早于 B.js。那么以后访问 B 页面就同样存在资源版本失配的隐患。

不使用must-revalidate,只是Cache-Control: max-age=600,浏览器缓存的自动清理机制就不会执行么?如果浏览器缓存的自动清理机制执行的话那后续的index.js被清掉的所引发的情况都是一样的呀!

这个问题涉及几个小点,我补充说明一下:

  • ‘max-age=600’ 和 ‘max-age=600,must-revalidate’ 有什么区别?

没有区别。在列出 max-age 了之后,must-revalidate 是否列出效果相同,浏览器都会在超过 max-age 之后进行校验,验证缓存是否可用。

在 HTTP 的规范中,只阐述了 must-revalidate 的作用,却没有阐述不列出 must-revalidate 时,浏览器应该如何解决缓存过期的问题,因此这其实是浏览器实现时的自主决策。(可能有少数浏览器选择在源站点无法访问时继续使用过期缓存,但这取决于浏览器自身)

  • 那 ‘max-age=600’ 是不是也会引发问题?

是的。问题的出现和是否列出 ‘must-revalidate’ 无关,依然会存在 JS CSS等文件版本失配的问题。因此常规的网站在不同页面需要使用不同的 JS CSS 文件时,如果要使用 max-age 做强缓存,不要设置一个太短的时间。

  • 那这类比较短的 max-age 到底能用在哪里呢?

既然版本存在失配的问题,那么要避开这个问题,就有两种方法。

整站都使用相同的 JS 和 CSS,即合并后的文件。这个比较适合小型站点,否则可能过于冗余,影响性能。(不过可能还是会因为浏览器自身的清理策略被清理,依然有隐患) 资源是独立使用的,并不需要和其他文件配合生效。例如 RSS 就归在此类。

# 使用 CDN

基本原理:CDN的全称是Content Delivery Network,即内容分发网络,选择一个最近的边缘节点获取所需的资源,如果没有就会往上寻找,直到源文件,最后缓存下来,其他用户就可以直接使用最近的边缘节点内容。

# 减少重排(回流)(Reflow)

基本原理:重排是 DOM 的变化影响到了元素的几何属性(宽和高),浏览器会重新计算元素的几何属性,会使渲染树中受到影响的部分失效,浏览器会验证 DOM 树上的所有其它结点的 visibility 属性,这也是 Reflow 低效的原因。如果 Reflow 的过于频繁,CPU 使用率就会急剧上升。

TIP

回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。 重绘不一定导致回流,回流一定会导致重绘。

注意

减少 Reflow,如果需要在 DOM 操作时添加样式,尽量使用 增加 class 属性,而不是通过 style 操作样式。

发生重绘回流

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

可以采取以下的方式:

//使用cssText
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

//修改CSS的class
const el = document.getElementById('test');
el.className += ' active'; 

# addEventListener减少监听事件的绑定

# 减少dom操作,合并操作

DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。 操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

减少 DOM 操作的核心思路,就是让 JS 去给 DOM 分压。

JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。

  • 使元素脱离文档流
  • 对其进行多次修改
  • 将元素带回到文档中。 该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。过“桥”要收费——这个开销本身就是不可忽略的。

方式

有三种方式可以让DOM脱离文档流:

  • 隐藏元素,应用修改,重新显示
  • 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
  • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

考虑我们要执行一段批量插入节点的代码:

隐藏元素,应用修改,重新显示 这个会在展示和隐藏节点的时候,产生两次重绘

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment); 

DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题。

# 避免触发同步布局事件

上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
} 

这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。 每一次循环都会强制浏览器刷新队列 。我们可以优化为:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

# 服务端渲染

服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。

# 懒加载和预加载

<div data-v-b2db8566="" 
    data-v-009ea7bb="" 
    data-v-6b46a625=""   
    data-src="https://user-gold-cdn.xitu.io/2018/9/27/16619f449ee24252?imageView2/1/w/120/h/120/q/85/format/webp/interlace/1"    
    class="lazy thumb thumb"    
    style="background-image: none; background-size: cover;">  
</div>

背景图片设置为了 none。也就是说这个 div 是没有内容的,它只起到一个占位的作用,一旦通过滚动使得这个 div 出现在了可见范围内,那么 div 元素的内容就会发生变化,呈现如下的内容

style="background-image: url(&quot;https://user-gold-cdn.xitu.io/2018/9/27/16619f449ee24252?imageView2/1/w/120/h/120/q/85/format/webp/interlace/1&quot;); background-size: cover;"

注意节流和抖动。

# 阻塞渲染

首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。

然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。

当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。

当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。

对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。

# 参考资料

浏览器在加载页面的时候会用到 GUI 渲染线程和 JavaScript 引擎线程。其中,GUI 渲染线程负责渲染浏览器界面 HTML 元素,JavaScript 引擎线程主要负责处理 JavaScript 脚本程序。

由于 JavaScript 在执行过程中还可能会改动界面结构和样式,因此它们之间被设计为互斥的关系。也就是说,当 JavaScript 引擎执行时,GUI 线程会被挂起。

以拉勾官网为例,我们来看看网页加载流程。

  1. 当我们打开拉勾官网的时候,浏览器会从服务器中获取到 HTML 内容。
  2. 浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素。
  3. <head>元素内容会先被解析,此时浏览器还没开始渲染页面。
我们看到<head>元素里有用于描述页面元数据的<meta>元素,还有一些<link>元素涉及外部资源(如图片、CSS 样式等),此时浏览器会去获取这些外部资源。
除此之外,我们还能看到<head>元素中还包含着不少的<script>元素,这些<script>元素通过src属性指向外部资源。
  1. 当浏览器解析到这里时(步骤 3),会暂停解析并下载 JavaScript 脚本。
  2. 当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,控制权会交回给渲染引擎,渲染引擎继续往下解析 HTML 页面。
  3. 此时<body>元素内容开始被解析,浏览器开始渲染页面。
在这个过程中,我们看到<head>中放置的<script>元素会阻塞页面的渲染过程:把 JavaScript 放在<head>里,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面。
到这里,我们就明白了:如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,用户体验会变得很糟糕。
因此,对于对性能要求较高、需要快速将内容呈现给用户的网页,常常会将 JavaScript 脚本放在<body>的最后面。这样可以避免资源阻塞,页面得以迅速展示。我们还可以使用defer/async/preload等属性来标记<script>标签,来控制 JavaScript 的加载顺序。

# 优化隐藏类和删除操作

  • V8引擎会添加隐藏类,但是如果动态添加新属性,本来可以共享的隐藏类就只能分开成多个了,且如果删除,也是同样的结果。
  • 最后不要动态添加属性,初始属性宁可undefined,同时如果某个属性不需要,置空即可
function Article() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
}

let a1 = new Article();
let a2 = new Article();

a2.author = 'Jake';
console.log(a1)//Article {title: 'Inauguration Ceremony Features Kazoo Band'}
console.log(a2)//Article {title: 'Inauguration Ceremony Features Kazoo Band', author: 'Jake'}


function Article1(opt_author) {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
  this.author = opt_author;
}

let a3 = new Article1();
let a4 = new Article1('Jake');
console.log(a3)//Article1 {title: 'Inauguration Ceremony Features Kazoo Band', author: undefined}
console.log(a4)//Article1 {title: 'Inauguration Ceremony Features Kazoo Band', author: 'Jake'}


function Article2() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
  this.author = 'Jake';
}

let a5 = new Article2();
let a6 = new Article2();

delete a5.author;
console.log(a5)//Article2 {title: 'Inauguration Ceremony Features Kazoo Band'}
console.log(a6)//Article2 {title: 'Inauguration Ceremony Features Kazoo Band', author: 'Jake'}


function Article3() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
  this.author = 'Jake';
}

let a7 = new Article3();
let a8 = new Article3();
a8.author =null
console.log(a7)//Article3 {title: 'Inauguration Ceremony Features Kazoo Band', author: 'Jake'}
console.log(a8)//Article3 {title: 'Inauguration Ceremony Features Kazoo Band', author: null}

# gzip压缩

gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右。

//npm i -D compression-webpack-plugin
configureWebpack: config => {
  const CompressionPlugin = require('compression-webpack-plugin')
  config.plugins.push(new CompressionPlugin())
}

# 去除 console.log

线上项目自然不应该被看到控制台的打印日志,所以我们需要将 console.log 都去除掉。

//npm i -D terser-webpack-plugin 
 configureWebpack: config => {
   const TerserPlugin = require('terser-webpack-plugin')
   config.optimization.minimizer.push(
     new TerserPlugin({
       extractComments: false,
       terserOptions: { compress: { drop_console: true } },
     })
   )
 }

# 去除 SourceMap

由于打包后的文件经过了压缩、合并、混淆、babel编译后的代码不利于定位分析bug。

module.exports = {
  productionSourceMap: false,
}

# 静态分配与对象池 ??

# 代码解耦

  • 解耦HTML/JS
  • CSS/JS(js尽量不直接改css样式,利用添加移除类修改)
  • function一个功能一个function,不要写在一起杂糅,传参也要尽量少
  • 常量定义
  • 全局变量少用,尽量包裹起来
  • 多次使用到的方法,提取,来减少复杂度-
  • 优化循环,减小循环体
  • 循环次数少且已知,可展开循环,多次请求;这样可以节省创建循环,计算终止条件的消耗。
// eliminated the loop
process(values[0]);
process(values[1]);
process(values[2]);
  • switch快于if。。。else
  • 语句最少化,能合并的不要分开写
let name = values[i];
i++;
//====>let name = values[i++];一句代码解决
  • 达夫设备优化
	// credit: Jeff Greenberg for JS implementation of Duff's Device
	// assumes values.length > 0
	let values={
		length:81
	}
	let iterations = Math.ceil(values.length / 8);
	let startAt = values.length % 8;
	let i = 0;
	           
	do {
	  switch(startAt) {
	    case 0: process(values[i++],0);
	    case 7: process(values[i++],7);
	    case 6: process(values[i++]);
	    case 5: process(values[i++]);
	    case 4: process(values[i++]);
	    case 3: process(values[i++]);
	    case 2: process(values[i++]);
	    case 1: process(values[i++]);
	  }
	  startAt = 0;
	} while (--iterations > 0);
	
	function process(v,v1){
		console.log(v,v1,startAt)
	}

前端优化大概可以有以下几个方向:

网络优化 页面渲染优化 JS优化 图片优化 webpack打包优化 React优化 Vue优化

网络优化 DNS预解析 link标签的rel属性设置dns-prefetch,提前获取域名对应的IP地址 使用缓存 减轻服务端压力,快速得到数据(强缓存和协商缓存可以看这里) 使用 CDN(内容分发网络) 用户与服务器的物理距离对响应时间也有影响。 内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。 压缩响应 压缩组件通过减少 HTTP 请求产生的响应包的大小,从而降低传输时间的方式来提高性能。从 HTTP1.1 开始,Web 客户端可以通过 HTTP 请求中的 Accept-Encoding 头来标识对压缩的支持(这个请求头会列出一系列的压缩方法) 如果 Web 服务器看到请求中的这个头,就会使用客户端列出的方法中的一种来压缩响应。Web 服务器通过响应中的 Content-Encoding 头来告知 Web 客户端使用哪种方法进行的压缩 目前许多网站通常会压缩 HTML 文档,脚本和样式表的压缩也是值得的(包括 XML 和 JSON 在内的任何文本响应理论上都值得被压缩)。但是,图片和 PDF 文件不应该被压缩,因为它们本来已经被压缩了。 使用多个域名 Chrome 等现代化浏览器,都会有同域名限制并发下载数的情况,不同的浏览器及版本都不一样,使用不同的域名可以最大化下载线程,但注意保持在 2~4 个域名内,以避免 DNS 查询损耗。 避免图片src为空 虽然 src 属性为空字符串,但浏览器仍然会向服务器发起一个 HTTP 请求: IE 向页面所在的目录发送请求; Safari、Chrome、Firefox 向页面本身发送请求; Opera 不执行任何操作。 页面渲染优化 Webkit 渲染引擎流程:

处理 HTML 并构建 DOM 树 处理 CSS 构建 CSS 规则树(CSSOM) DOM Tree 和 CSSOM Tree 合成一棵渲染树 Render Tree。 根据渲染树来布局,计算每个节点的位置 调用 GPU 绘制,合成图层,显示在屏幕上

避免css阻塞 css影响renderTree的构建,会阻塞页面的渲染,因此应该尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)的将css资源加载 降低css选择器的复杂度 浏览器读取选择器,遵循的原则是从选择器的右边到左边读取。

减少嵌套:最多不要超过三层,并且后代选择器的开销较高,慎重使用 避免使用通配符,对用到的元素进行匹配即可 利用继承,避免重复匹配和定义 正确使用类选择器和id选择器

避免使用CSS 表达式 css 表达式会被频繁地计算。 避免js阻塞 js可以修改CSSOM和DOM,因此js会阻塞页面的解析和渲染,并且会等待css资源的加载。也就是说js会抢走渲染引擎的控制权。所以我们需要给js资源添加defer或者async,延迟js脚本的执行。 使用外链式的js和css 在现实环境中使用外部文件通常会产生较快的页面,因为 JavaScript 和 CSS 有机会被浏览器缓存起来。对于内联的情况,由于 HTML 文档通常不会被配置为可以进行缓存的,所以每次请求 HTML 文档都要下载 JavaScript 和 CSS。所以,如果 JavaScript 和 CSS 在外部文件中,浏览器可以缓存它们,HTML 文档的大小会被减少而不必增加 HTTP 请求数量。 使用字体图标 iconfont 代替图片图标

图片会增加网络请求次数,从而拖慢页面加载时间 iconfont可以很好的缩放并且不会添加额外的请求

首屏加载优化

使用骨架屏或者动画优化用户体验 资源按需加载,首页不需要的资源延迟加载

减少重绘和回流

增加多个节点使用documentFragment:不是真实dom的部分,不会引起重绘和回流

用 translate 代替 top ,因为 top 会触发回流,但是translate不会。所以translate会比top节省了一个layout的时间

使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局);opacity 代替 visiability,visiability会触发重绘(paint),但opacity不会。

把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来

不要把 DOM 结点的属性值放在一个循环里当成循环里的变量 for (let i = 0; i < 1000; i++) { // 获取 offsetTop 会导致回流,因为需要去获取正确的值 console.log(document.querySelector('.test').style.offsetTop) } 复制代码

尽量少用table布局,table布局的话,每次有单元格布局改变,都会进行整个tabel回流重绘;

最好别频繁去操作DOM节点,最好把需要操作的样式,提前写成class,之后需要修改。只需要修改一次,需要修改的时候,直接修改className,做成一次性更新多条css DOM属性,一次回流重绘总比多次回流重绘要付出的成本低得多;

动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

每次访问DOM的偏移量属性的时候,例如获取一个元素的scrollTop、scrollLeft、scrollWidth、offsetTop、offsetLeft、offsetWidth、offsetHeight之类的属性,浏览器为了保证值的正确也会回流取得最新的值,所以如果你要多次操作,最取完做个缓存。更加不要for循环中访问DOM偏移量属性,而且使用的时候,最好定义一个变量,把要需要的值赋值进去,进行值缓存,把回流重绘的次数减少;

将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

JS中的性能优化 使用事件委托 防抖和节流 尽量不要使用JS动画 css3动画和canvas动画都比JS动画性能好 多线程 复杂的计算开启webWorker进行计算,避免页面假死 计算结果缓存 减少运算次数,比如vue中的computed 图片的优化 雪碧图 借助减少http请求次数来进行优化 图片懒加载 在图片即将进入可视区域的时候进行加载(判断图片进入可视区域请参考这里) 使用CSS3代替图片 有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好 图片压缩 压缩方法有两种,一是通过在线网站进行压缩,二是通过 webpack 插件 image-webpack-loader。它是基于 imagemin 这个 Node 库来实现图片压缩的。 使用渐进式jpeg 使用渐进式jpeg,会提高用户体验 参考文章 使用 webp 格式的图片 webp 是一种新的图片文件格式,它提供了有损压缩和无损压缩两种方式。在相同图片质量下,webp 的体积比 png 和 jpg 更小。 webpack打包优化 缩小loader 匹配范围

优化loader配置 test、include、exclude三个配置项来缩⼩loader的处理范围 推荐include

include: path.resolve(__dirname, "./src"), 复制代码 resolve.modules resolve.modules用于配置webpack去哪些目录下寻找第三方模块,默认是 node_modules。 寻找第三方,默认是在当前项目目录下的node_modules里面去找,如果没有找到,就会去上一级目录../node_modules找,再没有会去../../node_modules中找,以此类推,和Node.js的模块寻找机制很类似。 如果我们的第三⽅模块都安装在了项⽬根⽬录下,就可以直接指明这个路径。 module.exports={ resolve:{ modules: [path.resolve(__dirname, "./node_modules")] } }

复制代码 resolve.extensions resolve.extensions在导⼊语句没带⽂件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存在。

后缀尝试列表尽量的⼩ 导⼊语句尽量的带上后缀。

如果想优化到极致的话,不建议用extensionx, 因为它会消耗一些性能。虽然它可以带来一些便利。 抽离css 借助mini-css-extract-plugin:本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。。 const MiniCssExtractPlugin = require("mini-css-extract-plugin"); { test: /.less$/, use: [ // "style-loader", // 不再需要style-loader,⽤MiniCssExtractPlugin.loader代替 MiniCssExtractPlugin.loader, "css-loader", // 编译css "postcss-loader", "less-loader" // 编译less ] }, plugins: [ new MiniCssExtractPlugin({ filename: "css/[name][contenthash:6].css", chunkFilename: "[id].css" }) ] 复制代码 代码压缩 JS代码压缩 mode:production,使用的是terser-webpack-plugin module.exports = {    // ...    optimization: {        minimize: true,        minimizer: [            new TerserPlugin({}),       ]   } } 复制代码 CSS代码压缩 css-minimizer-webpack-plugin module.exports = {    // ...    optimization: {        minimize: true,        minimizer: [            new CssMinimizerPlugin({})       ]   } } 复制代码 Html文件代码压缩 module.exports = {    ...    plugin:[        new HtmlwebpackPlugin({            ...            minify:{                minifyCSS:false, // 是否压缩css                collapseWhitespace:false, // 是否折叠空格                removeComments:true // 是否移除注释           }       })   ] } 复制代码 设置了minify,实际会使用另一个插件html-minifier-terser 文件大小压缩 对文件的大小进行压缩,减少http传输过程中宽带的损耗 npm install compression-webpack-plugin -D 复制代码 new ComepressionPlugin({    test:/.(css|js)$/,  // 哪些文件需要压缩    threshold:500, // 设置文件多大开始压缩    minRatio:0.7, // 至少压缩的比例    algorithm:"gzip", // 采用的压缩算法 }) 复制代码 图片压缩 一般来说在打包之后,一些图片文件的大小是远远要比 js 或者 css 文件要来的大,所以图片压缩较为重要 配置方法如下: module: {  rules: [   {      test: /.(png|jpg|gif)$/,      use: [       {          loader: 'file-loader',          options: {            name: '[name][hash].[ext]',            outputPath: 'images/',         }       },       {          loader: 'image-webpack-loader',          options: {            // 压缩 jpeg 的配置            mozjpeg: {              progressive: true,              quality: 65           },            // 使用 imagemin**-optipng 压缩 png,enable: false 为关闭            optipng: {              enabled: false,           },            // 使用 imagemin-pngquant 压缩 png            pngquant: {              quality: '65-90',              speed: 4           },            // 压缩 gif 的配置            gifsicle: {              interlaced: false,           },            // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式            webp: {              quality: 75           }         }       }     ]   }, ] } 复制代码 Tree shaking 去除死代码 Tree Shaking 是一个术语,在计算机中表示消除死代码,依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系) 在webpack实现Tree shaking有两种不同的方案:

usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的 sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用

两种不同的配置方案, 有不同的效果 usedExports 配置方法也很简单,只需要将usedExports设为true module.exports = {    ...    optimization:{        usedExports   } } 复制代码 使用之后,没被用上的代码在webpack打包中会加入unused harmony export mul注释,用来告知 Terser 在优化时,可以删除掉这段代码 sideEffects sideEffects用于告知webpack compiler哪些模块时有副作用,配置方法是在package.json中设置sideEffects属性 如果sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports 如果有些文件需要保留,可以设置为数组的形式 "sideEffecis":[    "./src/util/format.js",    "*.css" // 所有的css文件 ] 复制代码 上述都是关于javascript的tree shaking,css同样也能够实现tree shaking css tree shaking css进行tree shaking优化可以安装PurgeCss插件 npm install purgecss-plugin-webpack -D 复制代码 const PurgeCssPlugin = require('purgecss-webpack-plugin') module.exports = {    ...    plugins:[        new PurgeCssPlugin({            path:glob.sync(${path.resolve('./src')}/**/*), {nodir:true}// src里面的所有文件            satelist:function(){                return {                    standard:["html"]               }           }       })   ] } 复制代码

paths:表示要检测哪些目录下的内容需要被分析,配合使用glob 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性

babel-plugin-transform-runtime减少ES6转化ES5的冗余 Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数。在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。 代码分离 将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件 默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度 代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能 这里通过splitChunksPlugin来实现,该插件webpack已经默认安装和集成,只需要配置即可 默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all module.exports = {    ...    optimization:{        splitChunks:{            chunks:"all"       }   } } 复制代码 splitChunks主要属性有如下:

Chunks,对同步代码还是异步代码进行处理 minSize: 拆分包的大小, 至少为minSize,如何包的大小不超过minSize,这个包不会拆分 maxSize: 将大于maxSize的包,拆分为不小于minSize的包 minChunks:被引入的次数,默认是1

多线程打包提升打包速度 vue

v-for添加key 路由懒加载 第三方插件按需引入 合理使用computed和watch v-for的同时避免使用v-if destory时销毁事件:比如addEventListener添加的事件、setTimeout、setInterval、bus.$on绑定的监听事件等

react

map循环展示添加key

路由懒加载

第三方插件按需引入

使用scu,memo或者pureComponent避免不必要的渲染

合理使用useMemo、memo、useCallback 他们三个的应用场景都是缓存结果,当依赖值没有改变时避免不必要的计算或者渲染。

useCallback 是针对函数进行“记忆”的,当它依赖项没有发生改变时,那么该函数的引用并不会随着组件的刷新而被重新赋值。当我们觉得一个函数不需要随着组件的更新而更新引用地址的时候,我们就可以使用 useCallback 去修饰它。 React.memo 是对组件进行 “记忆”,当它接收的 props 没有发生改变的时候,那么它将返回上次渲染的结果,不会重新执行函数返回新的渲染结果。 React.useMemo是针对 值计算 的一种“记忆“,当依赖项没有发生改变时,那么无需再去计算,直接使用之前的值,对于组件而言,这带来的一个好处就是,可以减少一些计算,避免一些多余的渲染。当我们遇到一些数据需要在组件内部进行计算的时候,可以考虑一下 React.useMemo

作者:let_code 链接:https://juejin.cn/post/7194400984490049573 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

# 参考文献

最后更新: 2/7/2024, 4:39:07 PM