1

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
2.1 DNS 解析
2.2 TCP 连接
2.3 发送 HTTP 请求
2.4 服务器响应
2.5 浏览器解析渲染页面
2.6 其他
2.7 小结
三 浏览器缓存
3.1 缓存位置
3.2 缓存机制
四 Cookie、Web Storage 和 IndexDB
4.1 Cookie
4.2 Local Storage
4.3 Session Storage
4.4 IndexDB
五 CDN
六 负载均衡
七 Webpack 优化
7.1 针对 Webpack 本身构建优化
  7.1.1 优化 resolve.modules 配置
  7.1.2 优化 resolve.extensions 配置
7.2 通过 Loader 和 Plugin 优化
  7.2.1 babel-loader
  7.2.2 tree shaking
  7.2.3 可视化分析
  7.2.4 缓存
  7.2.5 多进程
  7.2.6 抽离
  7.2.7 多进程代码压缩
  7.2.8 拆包
  7.2.9 打包资源压缩
  7.2.10 按需加载
7.3 优化体验
八 图片优化
8.1 JPEG 与 JPG
8.2 PNG-8 与 PNG-24
8.3 GIF
8.4 SVG
8.5 Base64
8.6 雪碧图
8.7 WebP
九 Gzip 压缩
十 服务端渲染
10.1 客户端渲染和服务端渲染
10.2 解决的性能问题
10.3 如何使用服务端渲染
10.4 服务端渲染小结
十一 浏览器渲染机制
11.1 浏览器渲染步骤
11.2 优化 - CSS 选择器问题
11.3 优化 - CSS 加载问题
11.4 优化 - JS 加载问题
11.5 优化 - DOM 渲染问题
十二 预加载页面资源
十三 长列表
13.1 懒加载
13.2 可视区域渲染
十四 性能监控
十五 参考文献
15.1 Webpack 优化
15.2 其他优化

二 前言

返回目录
  • 面试官:讲讲前端性能优化?

要说起前端性能优化,其实我们可以从 “输入 URL 到页面呈现” 这个知识点着手讲起。

在用户输入 URL,按下回车之后,走过的步骤:

  1. DNS 解析
  2. TCP 连接
  3. 发送 HTTP 请求
  4. 服务器响应
  5. 浏览器解析渲染页面

这其中可以做到哪些优化呢?

jsliang 在这里将这些知识点一锅炖,看你吃下多少。

2.1 DNS 解析

返回目录

DNS 解析过程是一个知识点,详细可看:计算机网络 - DNS

首先需要知道的是 DNS 解析的开始步骤:浏览器 DNS 缓存 -> 系统缓存(host) -> 路由器缓存

浏览器 DNS 缓存:你不确定,也无法帮用户缓存;

系统缓存(host):你自己修改 host 文件都要权限,修改用户的就更不靠谱了;

路由器缓存:用户家的路由器……

然后本地服务器向根服务器、顶级域名服务器、主域名服务器这些的请求就更不用说了,前端没法接触。

所以这个步骤我们忽略先。

2.2 TCP 连接

返回目录

计算机网络 - TCP 3 次握手和 4 次挥手……

这个步骤我们也忽略,前端性能优化暂时管不到它。

2.3 发送 HTTP 请求

返回目录

发送 HTTP 请求这块,我们可以通过 4 点进行讲解:

  • 浏览器缓存

HTTP 请求发起的时候,我们可以利用浏览器缓存,看采用强缓存还是协商缓存,这样我们对于有缓存的页面可以快速加载。

  • Cookie 和 WebStorage

利用 CookieWebStorage 对一些无关紧要的数据进行缓存,方便利用。

  • CDN 的使用

静态资源的请求可以采用 CDN,减少服务器压力、防止不必要携带 Cookie 的场景等。

  • 负载均衡

利用负载均衡的特点,开启 Node.js 方面的 PM2 或者 Nginx 的反向代理,轮询服务器,平均各个服务器的压力。

2.4 服务器响应

返回目录

在服务器响应的时候,我们也可以做 4 部分:

  • Webpack 优化

在发布项目到服务器之前,我们可以利用一些可视化插件进行分析,使用 Happypack 等提高打包效率,项目内容上可以做按需加载、tree shaking 等。

  • 图片优化

我们需要熟悉了解 JPG/JPEGPNG-8/PNG-24GIFBase64SVG 这些图片的特性,然后通过 Webpack 的 url-loader 将一些小图标转换成 Base64,一些 Icon 使用 SVG,一些轮播图、Banner 图用 JPG/JPGE、雪碧图的使用等。

  • Gzip 压缩

Gzip 压缩的原理是在一个文本文件中找一些重复出现的字符串、临时替换它们,从而使整个文件变小(对于图片等会处理不了)。我们可以通过 Webpack 的 ComparessionPlugin 进行 Gzip 压缩,然后在 Nginx 上进行配置,就可以利用好 Gzip 了。

  • 服务端渲染(SSR)

服务端渲染是指浏览器请求的时候,服务端将已经渲染好的 HTML 页面直接返回给浏览器,浏览器直接加载它的 HTML 渲染即可,减少了前后端交互,对 SEO 更友好。

2.5 浏览器解析渲染页面

返回目录

浏览器解析渲染页面的过程是:

  1. 解析 HTML,生成 DOM
  2. 解析 CSS,生成 CSS 规则树(CSS Rule Tree)
  3. DOM TreeCSS Rule Tree 相结合,生成 渲染树Render Tree
  4. 从根节点开始,计算每一个元素的大小、位置,给出每个节点所应该出现的屏幕精确坐标,从而得到基于渲染树的 布局渲染树Layout of the render tree)。
  5. 遍历渲染树,将每个节点用 UI 渲染引擎来绘制,从而将整棵树绘制到页面上,这个步骤叫 绘制渲染树Painting the render tree

关于这个步骤我们的优化方案有:

  1. CSS 选择器解析问题。编码过程中用尽可能少的选择器来表示一个元素,因为 CSS 是从右往左加载的。
  2. CSS 加载问题。尽可能在 head 位置加载 CSS,减少 HTML 加载完毕需要等待 CSS 加载的问题。
  3. JS 加载问题。JS 的加载会阻塞 HTML 和 CSS 的加载,所以 script 标签通常放 body 后面,同时可以利用 script 标签的 asyncdefer 属性,同步加载 JS 或者等 HTML 和 CSS 加载渲染完后再加载 JS。
  4. DOM 渲染问题。DOM 渲染的时候可能会触发回流和重绘,应该尽量避免触发。

如何避免触发回流:

  1. 【CSS】使用 visibility 替换 display
  2. 【CSS】避免 table 布局。对于 Render Tree 的计算通常只需要遍历一次就可以完成,但是 table 布局需要计算多次,通常要花 3 倍于等同元素的时间,因此要避免。
  3. 【JS】避免频繁做 widthheight 等会触发回流的操作。
  4. 【JS】操作 DOM 的时候,如果是添加 DOM 节点,可以将所有节点都在 JS 中操作完毕,再进行渲染(一次性)

2.6 其他

返回目录

除此之外,我们还可以通过:

  • Chrome 插件可视化判断页面哪些部分可进行优化
  • 长列表使用懒加载
  • preload 预加载页面

等进行性能优化相关操作。

2.7 小结

返回目录

以上,我们就通过 6 个部分,串起来讲解了前端性能优化部分的知识点。

当然,肯定有我们没有顾及的地方,欢迎小伙伴评论留言吐槽或者私聊 jsliangjsliang 会逐步完善这块内容。

下面我们逐一详细的过一下上面讲到的优化知识点。

三 浏览器缓存

返回目录

浏览器缓存可以简单地理解为 HTTP 缓存。

3.1 缓存位置

返回目录

浏览器缓存位置分 4 个部分:

  • Service Worker Cache - 运行在浏览器背后的独立线程。一般可以用来实现缓存功能。
  • Menory Cache - 内存中的缓存。主要是页面上已经下载的样式、脚本、图片等已经抓取到的资源。
  • Disk Cache - 硬盘中的缓存。读取速度相对慢点。
  • Push Cache - 推送缓存。 是 HTTP2 中的内容,当以上 3 种缓存都没有命中的时候,它才会被使用。

3.2 缓存机制

返回目录
  • 强缓存

强缓存优先于协商缓存进行,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存。强缓存不会向服务器发送请求,直接从缓存中读取资源。

强缓存利用 HTTP 请求头的 ExpiresCache-Control 两个字段来控制。

  • 协商缓存

协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器中;生效则返回 304,继续使用缓存。

协商缓存利用 Last-Modified + If-Modified-SinceEtag + If-None-Match 来实现。

具体的缓存过程小伙伴们可以看浏览器缓存篇章,这里就不哆嗦了:

四 Cookie、Web Storage 和 IndexDB

返回目录

4.1 Cookie

返回目录

Cookie 最开始被设计出来其实并不是来做本地存储的,而是为了弥补 HTTP 在状态管理上的不足。

Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储。

向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。

缺陷:

  1. 容量缺陷。体积上线 4kb,只能存储少量信息。
  2. 性能缺陷Cookie 请求每次都会携带上完整的 Cookie,随着请求数增多,造成性能浪费。
  3. 安全缺陷。以纯文本的形式在浏览器和服务器中传递,容易被非法截获和篡改。

4.2 Local Storage

返回目录

Local Storge 也是针对同一个域名。

同一个域名下,会存储相同的一段 Local Storage

相比 Cookie 优势:

  1. 容量。体积上线 5M,大于 Cookie4kb
  2. 只存在客户端。不参与和服务端的通讯,避免 Cookie 的性能缺陷和安全缺陷。
  3. 接口封装。有 setItemgetItem 两个 API 接口。

应用场景

  • Base64 方式存储官方 Logo 等图片。

4.3 Session Storage

返回目录

基本上和 Local Stoarge 一致。

相比较上的不同:

  • 会话级别的存储。不同于 Local Storage 的持续化存储,Session Storage 当页面关闭的时候就不复存在了。

应用场景

  1. 对表单信息做维护。用户刷新页面不丢失。
  2. 存储本次浏览记录。看过的页面不怕找不到。

4.4 IndexDB

返回目录

IndexedDB 是运行在浏览器中的 非关系型数据库

因为本质上是数据库,所以一般来说容量是没有上线的。

五 CDN

返回目录

CDN(Content Delivery Network,内容分发网络)指的是一组分布在各个地区的服务器。

这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。

CDN 提供快速服务,较少受高流量影响。

假设有一部影片出版,非常多人看。jsliang 在广州,请求上海的服务器,结果这个服务器非常多人,资源响应地很慢。于是 jsliang 切换了路线,看到深圳服务器也有这个资源,于是向深圳服务器请求,结果能很快地看到这部影片。

在这个场景中,深圳服务器就扮演 CDN 的角色。

CDN 的核心:缓存回源

  • 缓存:将资源 copy 一份到 CDN 服务器。
  • 回源:CDN 发现自己没有这个资源,转头向根服务器(上级服务器)请求这个资源。

应用场景

  1. 公司静态资源部署到就近的服务器,利用 CDN 特性方便访问
  2. jQuery 等框架可以引用 CDN,加快网站的加载速度,避免同一个服务器加载的限制。
  3. 减少 Cookie 影响。同一个域名下,请求静态资源会携带 Cookie 信息,但是我们并不需要,所以使用 CDN 可以避免不必要的 Cookie 出现场景。

六 负载均衡

返回目录

如果是大型网站,负载均衡是不可或缺的内容。

  • PM2:一款 Node.js 进程管理器,让计算机每一个内核都启动一个 Node.js 服务,并且实现自动控制负载均衡。
  • Nginx:通过轮询机制,将用户的请求分配到压力较小的服务器上(反向代理)。

区别:反向代理是对服务器实现负载均衡,而 PM2 是对进程实现负载均衡。

七 Webpack 优化

返回目录

Webpack 的优化瓶颈,主要是 2 个方面:

  • Webpack 的构建过程太花时间
  • Webpack 打包的结果体积太大

7.1 针对 Webpack 本身构建优化

返回目录

7.1.1 优化 resolve.modules 配置

返回目录

resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块,默认是 ['node_modules'],但是,它会先去当前目录的 ./node_modules 查找,没有的话再去 ../node_modules,最后到根目录。

所以可以直接指定项目根目录,就不需要一层一层查找。

resolve: {
  modules: [path.resolve(__dirname, 'node_modules')],
}

7.1.2 优化 resolve.extensions 配置

返回目录

在导入没带文件后缀的路径时,Webpack 会自动带上后缀去尝试询问文件是否存在,而 resolve.extensions 用于配置尝试后缀列表;默认为 extensions:['js', 'json']

当遇到 require('./data')Webpack 会先尝试寻找 data.js,没有再去找 data.json;如果列表越长,或者正确的后缀越往后,尝试的次数就会越多。

所以在配置时为提升构建优化需遵守:

  1. 频率出现高的文件后缀优先放在前面。
  2. 列表尽可能的少,例如只有 3 个:jsjsxjson
  3. 书写导入语句时,尽量写上后缀名。

7.2 通过 Loader 和 Plugin 优化

返回目录

7.2.1 babel-loader

返回目录

babel-loader 为例,可以通过 includeexclude 帮助我们避免 node_modules 这类庞大文件夹。

7.2.2 tree shaking

返回目录

通过 ES6 的 import/export 来检查未引用代码,以及 sideEffects 来标记无副作用代码,最后用 UglifyJSPlugin 来做 tree shaking,从而删除冗余代码。

7.2.3 可视化分析

返回目录
  • speed-measure-webpack-plugin:测量出在构建过程中,每一个 Loader 和 Plugin 的执行时长。
  • webpack-bundle-analyzer:通过矩阵树图的方式将包内各个模块的大小和依赖关系呈现出来。
  • webpack-chart
  • webpack-analyse

7.2.4 缓存

返回目录
  • cache-loader

参考链接:cache-loader

babel-loader 开启 cache 后,将 loader 的编译结果写进硬盘缓存,再次构建如果文件没有发生变化则会直接拉取缓存。

  • uglifyjs-webpack-plugin

也可以解决缓存问题。

7.2.5 多进程

返回目录

Happypack 可以将任务分解成多个子进程去并发执行,大大提升打包效率。

7.2.6 抽离

返回目录

通过 DllPlugin 或者 Externals 进行静态依赖包的分离。

由于 CommonsChunkPlugin 每次构建会重新构建一次 vendor,所以出于效率考虑,使用 DllPlugin 将第三方库单独打包到一个文件中,只有依赖自身发生版本变化时才会重新打包。

7.2.7 多进程代码压缩

返回目录

因为自带的 UglifyJsPlugin 压缩插件是单线程运行的,而 ParallelUglifyPlugin 可以并行执行。

所以通过 ParallelUglifyPlugin 代替自带的 UglifyJsPlugin 插件。

7.2.8 拆包

返回目录

Webpack 中,到底什么是代码分离?代码分离允许你把代码拆分到多个文件中。如果使用得当,你的应用性能会提高很多。因为浏览器能缓存你的代码。

每当你做出一次修改,包含修改的文件需要被所有访问你网站的人重新下载。但你并不会经常修改应用的依赖库。

如果你能把那些依赖库拆分到完全分离的文件中,即使业务逻辑发生了更改,访问者也不需要再次下载依赖库,直接使用之前的缓存就可以了。

由于有了 SplitChunksPlugin,你可以把应用中的特定部分移至不同文件。如果一个模块在不止一个 chunk 中被使用,那么利用代码分离,该模块就可以在它们之间很好地被共享。

7.2.9 打包资源压缩

返回目录
  • JS 压缩:UglifyJSPlugin
  • HTML 压缩:HtmlWebpackPlugin
  • 提取公共资源:splitChunks.cacheGroups
  • CSS 压缩:MiniCssExtractPlugin
  • Gzip 压缩:不包括图片

7.2.10 按需加载

返回目录

通过 Code-Splitting 来做 React 的按需加载.

Code_Splitting 核心是 require-ensure

7.3 优化体验

返回目录
  • progress-bar-webpack-plugin:在终端底部,将会有一个构建的进度条,可以让你清晰的看见构建的执行进度。
  • webpack-build-notifier:在构建完成时,能够像微信、Lark 这样的 APP 弹出消息的方式,提示构建已经完成。
  • webpack-dashboard:对 Webpack 原始的构建输出不满意的话,也可以使用这样一款 Plugin 来优化你的输出界面。

八 图片优化

返回目录

8.1 JPEG 与 JPG

返回目录
  • 关键字:有损压缩、体积小、加载快、不支持透明
  • 优点:压缩一定程度能保持品质、体积小、请求速度快
  • 缺点:处理矢量图形、Logo 等线条感较强,颜色对比强烈的图形,人为压缩会导致图片模糊明显。不支持透明度处理。
  • 使用场景:大的背景图、轮播图或者 Banner 图。

8.2 PNG-8 与 PNG-24

返回目录
  • 关键字:无损压缩、质量高、体积大、支持透明
  • 优点:PNG-8 支持 256 种颜色,PNG-24 支持 1600 种颜色。更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。
  • 缺点:体积较大
  • 使用场景:Logo、颜色简单且对比强烈的图片和背景。

8.3 GIF

返回目录
  • 关键字:动态图、体积小支持透明
  • 优点:可以压缩体积非常小。可插入多帧实现动画效果。支持透明色浮现于背景之上。
  • 缺点:最多只能处理 256 中颜色,不适用于真彩图像。
  • 使用场景:小动画。

8.4 SVG

返回目录
  • 关键字:文本文件、体积小、不失真、兼容性好
  • 优点:文本体积更小,可压缩性更强。图片可以无限放大不失真。文本文件可以直接在 HTML 中写入,灵活性高。
  • 缺点:渲染成本高、学习成本(可编程)
  • 使用场景:变成代码嵌入 HTML 中,也可以换成 .svg 后缀的文件进行引用。

8.5 Base64

返回目录
  • 关键字:文本文件、依赖编码、小图标解决方案
  • 优点:作为雪碧图的补充而存在,减少加载页面图片时对服务器的请求次数。(img src 会发起资源请求,但是 Base64 得到的是字符串,嵌入 HTML 中)
  • 缺点:大图使用 Base64 会增大体积,影响性能
  • 使用场景:小 Logo(不超过 2kb)、更新频率低的图片。
  • 编码工具:Webpack 的 url-loader 可以根据文件大小来判断是否编码成 Base64。

8.6 雪碧图

返回目录

雪碧图、CSS 精灵、CSS Sprites、图像精灵,都是同一个玩意。

它是将小图标和背景图像合并到一张图片上,然后通过 CSS 背景定位来显示其中的每一个具体部分。

它是一种优化手段,因为单张图片所需的 HTTP 请求更少,对内存和带宽更加友好。

8.7 WebP

返回目录
  • 关键字:年轻的全能型选手
  • 优点:支持有损压缩和无损压缩、支持透明、可以跟 GIF 一样显示动态图
  • 缺点:兼容性差
  • 使用场景:暂无大型应用场景

九 Gzip 压缩

返回目录
  • Webpack 开启 Gzip

通过 compression-webpack-plugin 可以开启 Gzip 压缩。

  • 是否值得开启 Gzip

如果压缩文件太小,那不使用;但是如果具有一定规模的项目文件,可以开启 Gzip。

  • Gzip 原理

Gzip 并不是万能的,它的原理是在一个文本文件中找一些重复出现的字符串、临时替换它们,从而使整个文件变小,所以对于图片等会处理不了。

  • 服务器端和 Webpack 的 Gzip 并存

服务器压缩也需要时间开销和 CPU 开销,所以有时候可以用 Webpack 来进行 Gzip 压缩,从而为服务器分压。

十 服务端渲染

返回目录
  • 什么是服务端渲染(服务端渲染的运行机制)
  • 为什么要用服务端渲染(服务端渲染解决了什么性能问题)
  • 怎么做服务端渲染(服务端渲染的应用实例和使用场景)

10.1 客户端渲染和服务端渲染

返回目录

客户端渲染中,页面上呈现的内容,在 HTML 源文件中往往找不到。

而服务端渲染,当用户第一次请求页面时,服务器会把需要的组件或者页面渲染成 HTML 字符串,返回给客户端。

即客户端直接拿到 HTML 内容,而不需要跑一遍 JS 去生成 DOM 内容。

“所见即所得”,服务端渲染情景下,页面上呈现的内容,在 HTML 源文件里面也可以找到。

10.2 解决的性能问题

返回目录

假设 A 网站关键字上有 前端性能优化,但是这篇文章只有 A 网站服务器搜索过后才会出来结果,这时候搜索引擎是无法找到的。

为了更好的 SEO 效果,就要拿 “现成的内容” 给搜索引擎看,就要开启服务端渲染。

其次,服务端渲染解决了一个性能问题 —— 首屏加载速度过慢。

从输入 URL 到页面渲染过程中我们知道,如果是客户端渲染,我们需要加载 HTML、CSS,然后再经过 JS 形成 Render Tree,定位后再绘制页面。

这个过程中用户一直在等待,如果采用了服务端渲染,那么服务端可以直接给一个可以拿来呈现给用户的页面。

10.3 如何使用服务端渲染

返回目录
  • 如何给 React 开启服务端渲染
  • 如何给 Vue 开启服务端渲染

给 React 开启:

前端项目 - VDOM.js
import React from 'react';

const VDom = () => {
  return <div>我是一个被渲染为真实 DOM 的虚拟 DOM</div>
};

export default VDom;
Node 项目 - index.js
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import VDom from './VDom';

// 创建一个 express 应用
const app = express();

// renderToString 是把虚拟 DOM 转化为真实 DOM 的关键方法
const RDom = renderToString(<VDom />);

// 编写 HTML 模板,插入转化后的真实 DOM 内容
const Page = `
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <span>服务端渲染出了真实 DOM:  </span>
    ${RDom}
  </body>
</html>
`;
            
// 配置 HTML 内容对应的路由
app.get('/index', function(req, res) {
  res.send(Page)
});

// 配置端口号
const server = app.listen(8000);

VDom 组件已经被 renderToString 转化为了一个内容为 <div data-reactroot="">我是一个被渲染为真实 DOM 的虚拟 DOM</div> 的字符串,这个字符串被插入 HTML 代码,成为了真实 DOM 树的一部分。

至于 Vue 的可以看:服务器端渲染 (SSR)?

不熟悉 Vue,就不哆嗦了。

10.4 服务端渲染小结

返回目录

SSR 主要用于解决单页应用首屏渲染慢以及 SEO 问题,同时也解决了与后端同学的沟通成本。但同时:提高了服务器压力,吃 CPU,内存等资源,优化不好提高成本。

十一 浏览器渲染机制

返回目录

浏览器内核决定了浏览器解释网页语法的方式。

目前常见的浏览器内核有:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。

11.1 浏览器渲染步骤

返回目录

如上图,浏览器的渲染过程为:

  1. 解析 HTML,生成 DOM
  2. 解析 CSS,生成 CSS 规则树(CSS Rule Tree)
  3. DOM TreeCSS Rule Tree 相结合,生成 渲染树Render Tree
  4. 从根节点开始,计算每一个元素的大小、位置,给出每个节点所应该出现的屏幕精确坐标,从而得到基于渲染树的 布局渲染树Layout of the render tree)。
  5. 遍历渲染树,将每个节点用 UI 渲染引擎来绘制,从而将整棵树绘制到页面上,这个步骤叫 绘制渲染树Painting the render tree

11.2 优化 - CSS 选择器问题

返回目录

我们正常的阅读顺序是从左往右的,但是 CSS 解析器解析 CSS 的时候,采用的是古人的规则。

#ul li {}

这样的一行规则,我们写起来的时候很顺畅:先找 idul 的元素,再找里面的 li 元素。

但是实际上 CSS 解析器是从右往左的,它会先查找所有 li 元素,并且逐个确认这个 li 元素的父元素的 id 是不是 ul,这就坑死了。

所以像通配符 * { padding: 0; margin: 0 } 这种,小伙伴们就应该减少设置,要不然页面的元素越多遍历匹配越久。

总结一下:

  • 避免使用通配符 * 等。
  • 减少使用标签选择器,用类选择器或者标签选择器替代,例如 span 替换为 .span
  • 减少嵌套匹配,例如 #ul li a

11.3 优化 - CSS 加载问题

返回目录

为了避免 HTML 解析完毕,但是 CSS 没有解析完毕,从而导致页面直接 “裸奔” 在用户面前的问题,浏览器在处理 CSS 规则树的时候,不会渲染任何已处理的内容。

所以很多时候,我们会让网页尽早处理 CSS,即在 head 标签中启用 link 或者启用 CDN 实现静态资源加载速度的优化。

11.4 优化 - JS 加载问题

返回目录

在上面的加载过程中我们并没有提到 JS,实际上 JS 会对 DOM 和 CSSDOM 进行修改,因此 JS 的执行会阻止 CSS 规则树的解析,有时候还会阻塞 DOM。

实际上,当 HTML 解析器遇上 script 标签时,它会暂停解析过程,将控制器交给 JS 引擎。

如果是内部的 JS 代码,它会直接执行,但是如果是 src 引入的,还要先获取脚本,再进行执行。

等 JS 引擎执行完毕后,再交接给渲染引擎,继续 HTML 树和 CSS 规则树的构建。

这样一来一回交接,而且有时候 JS 执行过多还会卡慢,进而导致页面渲染变慢。

所以我们可以通过 async 异步加载完 JS 脚本,再执行里面内容;或者通过 defer 等整个文档解析完毕后,再执行这个 JS 文件。

如果 JS 和 DOM 元素或者其他 JS 代码之间的依赖不强的时候,使用 async

如果 JS 依赖于 DOM 元素和其他 JS 的执行结果,那就使用 defer

11.5 优化 - DOM 渲染问题

返回目录

当使用 JS 去操作 DOM 的时候,实际上是 JS 引擎和渲染引擎之间的沟通,这个沟通的过程要开销的。

每操作一次 DOM 就收费一次,多了页面就卡起来咯。

同时,操作 DOM 的时候修改了尺寸等元素,还会引起回流和重绘。

  • 回流(reflow):又叫重排(layout)。当元素的尺寸、结构或者触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。
  • 重绘(repaint):当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要 UI 层面的重新像素绘制,因此损耗较少

什么操作触发回流?

  1. 添加删除 DOM 元素
  2. 改变边框、边距、宽高(bordermarginpaddingwidthheight
  3. 浏览器改变窗口(resize
  4. ……等

什么操作触发重绘?

  1. 修改背景色、颜色(backgroundcolor
  2. 设置可见度(visibility
  3. 设置背景图(background-image
  4. ……等

我们仔细看这张图,可以看到重排(Layout)会导致 Render Tree 重构,进而触发重绘(Painting):

  • 回流必定重绘,重绘不一定回流

因此,我们操作 DOM 的时候,可以这么优化:

  1. 【CSS】使用 visibility 替换 display
  2. 【CSS】避免 table 布局。对于 Render Tree 的计算通常只需要遍历一次就可以完成,但是 table 布局需要计算多次,通常要花 3 倍于等同元素的时间,因此要避免。
  3. 【JS】避免频繁做 widthheight 等会触发回流的操作。
  4. 【JS】操作 DOM 的时候,如果是添加 DOM 节点,可以将所有节点都在 JS 中操作完毕,再进行渲染(一次性)

十二 预加载页面资源

返回目录

preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。

提供的好处主要是:

  • 将加载和执行分离开,可不阻塞渲染和 documentonload 事件
  • 提前加载指定资源,不再出现依赖的 font 字体隔了一段时间才刷出
<!-- 使用 link 标签静态标记需要预加载的资源 -->
<link rel="preload" href="/path/to/style.css" as="style">

<!-- 或使用脚本动态创建一个 link 标签后插入到 head 头部 -->
<script>
  const link = document.createElement('link');
  link.rel = 'preload';
  link.as = 'style';
  link.href = '/path/to/style.css';
  document.head.appendChild(link);
</script>

在不支持 preload 的浏览器环境中,会忽略对应的 link 标签。

区分 preloadprefetch

  • preload:告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。
  • prefetch:告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。

当然,开发中需要注意:

  • 避免滥用 preload
  • 避免混用 preloadprefetch
  • 避免错用 preload 加载跨域资源

十三 长列表

返回目录

13.1 懒加载

返回目录

懒加载实现思路:

  • div 通过背景图片设置为 none,起到占位的作用。
  • 出现在可视区域的时候,div 填写有效 URL。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Lazy-Load</title>
  <style>
    .img {
      width: 100px;
      height: 300px;
    }
    .img img {
      width: 200px;
      height: 400px;
    }
  </style>
</head>
<body>
  <div class="container">
    <!-- 注意我们并没有为它引入真实的 src -->
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png"></div>
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png"></div>
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png"></div>
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png"></div>
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png"></div>
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png"></div>
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png"></div>
    <div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png"></div>
  </div>

  <script>
    (function() {
      // 获取所有的图片标签
      const imgs = document.getElementsByTagName('img');

      // 获取可视区域的高度(document.documentElement.clientHeight 是兼容低版本 IE)
      const viewHeight = window.innerHeight || document.documentElement.clientHeight;

      // num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
      let num = 0;

      const lazyload = () => {
        for (let i = num; i < imgs.length; i++) {

          // 用可视区域高度减去元素顶部距离可视区域顶部的高度
          let distance = viewHeight - imgs[i].getBoundingClientRect().top;

          // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
          if (distance >= 0){

            // 给元素写入真实的 src,展示图片
            imgs[i].src = imgs[i].getAttribute('data-src');

            // 前 i 张图片已经加载完毕,下次从第 i + 1 张开始检查是否露出
            num = i + 1;
          }
        }
      }

      // 首屏初始化
      lazyload();

      // 监听Scroll事件
      window.addEventListener('scroll', lazyload, false);
    })()
  </script>
</body>
</html>

还有其他方法,诸如:

  • img 标签自带的 loading 属性
  • InsectionObserver
  • 骨架屏

13.2 可视区域渲染

返回目录

无限滚动在移动端很常见,但是可见区域渲染并不常见,主要是因为 IOS 上 UIWebView 的 onscroll 并不能实时触发。

实现可见区域渲染的思路:

  1. 计算当前可见区域起始数据的 startIndex
  2. 计算当前可见区域结束数据的 endIndex
  3. 计算当前可见区域的数据,并渲染到页面中
  4. 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上

代码实现:略。

十四 性能监控

返回目录
  1. Chrome 工具 Performance
  2. Chrome 插件 Page Speed
  3. 自动化工具 Lighthouse

    1. Chrome 拓展安装
    2. npm i lighthouse -glighthouse https://www.baidu.com
    3. Chrome 有基于 LightHouseAudits 面板

十五 参考文献

返回目录

本篇参考文献有 31 篇。

15.1 Webpack 优化

返回目录

2019 年文章

2018 年文章

2017 年文章

15.2 其他优化

返回目录

jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。<br/>基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/>本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。

jsliang
393 声望31 粉丝

一个充满探索欲,喜欢折腾,乐于扩展自己知识面的终身学习斜杠程序员