一 目录
不折腾的前端,和咸鱼有什么区别
二 前言
返回目录
- 面试官:讲讲前端性能优化?
要说起前端性能优化,其实我们可以从 “输入 URL 到页面呈现” 这个知识点着手讲起。
在用户输入 URL
,按下回车之后,走过的步骤:
- DNS 解析
- TCP 连接
- 发送 HTTP 请求
- 服务器响应
- 浏览器解析渲染页面
这其中可以做到哪些优化呢?
jsliang 在这里将这些知识点一锅炖,看你吃下多少。
2.1 DNS 解析
返回目录
DNS 解析过程是一个知识点,详细可看:计算机网络 - DNS
首先需要知道的是 DNS
解析的开始步骤:浏览器 DNS 缓存 -> 系统缓存(host) -> 路由器缓存
浏览器 DNS
缓存:你不确定,也无法帮用户缓存;
系统缓存(host
):你自己修改 host
文件都要权限,修改用户的就更不靠谱了;
路由器缓存:用户家的路由器……
然后本地服务器向根服务器、顶级域名服务器、主域名服务器这些的请求就更不用说了,前端没法接触。
所以这个步骤我们忽略先。
2.2 TCP 连接
返回目录
这个步骤我们也忽略,前端性能优化暂时管不到它。
2.3 发送 HTTP 请求
返回目录
发送 HTTP
请求这块,我们可以通过 4
点进行讲解:
- 浏览器缓存
HTTP
请求发起的时候,我们可以利用浏览器缓存,看采用强缓存还是协商缓存,这样我们对于有缓存的页面可以快速加载。
- Cookie 和 WebStorage
利用 Cookie
和 WebStorage
对一些无关紧要的数据进行缓存,方便利用。
- CDN 的使用
静态资源的请求可以采用 CDN
,减少服务器压力、防止不必要携带 Cookie
的场景等。
- 负载均衡
利用负载均衡的特点,开启 Node.js 方面的 PM2 或者 Nginx 的反向代理,轮询服务器,平均各个服务器的压力。
2.4 服务器响应
返回目录
在服务器响应的时候,我们也可以做 4 部分:
- Webpack 优化
在发布项目到服务器之前,我们可以利用一些可视化插件进行分析,使用 Happypack
等提高打包效率,项目内容上可以做按需加载、tree shaking
等。
- 图片优化
我们需要熟悉了解 JPG/JPEG
、PNG-8/PNG-24
、GIF
、Base64
、SVG
这些图片的特性,然后通过 Webpack 的 url-loader
将一些小图标转换成 Base64
,一些 Icon 使用 SVG
,一些轮播图、Banner 图用 JPG/JPGE
、雪碧图的使用等。
- Gzip 压缩
Gzip 压缩的原理是在一个文本文件中找一些重复出现的字符串、临时替换它们,从而使整个文件变小(对于图片等会处理不了)。我们可以通过 Webpack 的 ComparessionPlugin
进行 Gzip 压缩,然后在 Nginx 上进行配置,就可以利用好 Gzip 了。
- 服务端渲染(SSR)
服务端渲染是指浏览器请求的时候,服务端将已经渲染好的 HTML 页面直接返回给浏览器,浏览器直接加载它的 HTML 渲染即可,减少了前后端交互,对 SEO 更友好。
2.5 浏览器解析渲染页面
返回目录
浏览器解析渲染页面的过程是:
- 解析 HTML,生成
DOM
树 - 解析 CSS,生成
CSS 规则树(CSS Rule Tree)
- 将
DOM Tree
和CSS Rule Tree
相结合,生成 渲染树(Render Tree
) - 从根节点开始,计算每一个元素的大小、位置,给出每个节点所应该出现的屏幕精确坐标,从而得到基于渲染树的 布局渲染树(
Layout of the render tree
)。 - 遍历渲染树,将每个节点用 UI 渲染引擎来绘制,从而将整棵树绘制到页面上,这个步骤叫 绘制渲染树(
Painting the render tree
)
关于这个步骤我们的优化方案有:
- CSS 选择器解析问题。编码过程中用尽可能少的选择器来表示一个元素,因为 CSS 是从右往左加载的。
- CSS 加载问题。尽可能在
head
位置加载 CSS,减少 HTML 加载完毕需要等待 CSS 加载的问题。 - JS 加载问题。JS 的加载会阻塞 HTML 和 CSS 的加载,所以
script
标签通常放body
后面,同时可以利用script
标签的async
和defer
属性,同步加载 JS 或者等 HTML 和 CSS 加载渲染完后再加载 JS。 - DOM 渲染问题。DOM 渲染的时候可能会触发回流和重绘,应该尽量避免触发。
如何避免触发回流:
- 【CSS】使用
visibility
替换display
- 【CSS】避免
table
布局。对于Render Tree
的计算通常只需要遍历一次就可以完成,但是table
布局需要计算多次,通常要花 3 倍于等同元素的时间,因此要避免。 - 【JS】避免频繁做
width
、height
等会触发回流的操作。 - 【JS】操作 DOM 的时候,如果是添加 DOM 节点,可以将所有节点都在 JS 中操作完毕,再进行渲染(一次性)
2.6 其他
返回目录
除此之外,我们还可以通过:
- Chrome 插件可视化判断页面哪些部分可进行优化
- 长列表使用懒加载
preload
预加载页面
等进行性能优化相关操作。
2.7 小结
返回目录
以上,我们就通过 6 个部分,串起来讲解了前端性能优化部分的知识点。
当然,肯定有我们没有顾及的地方,欢迎小伙伴评论留言吐槽或者私聊 jsliang,jsliang 会逐步完善这块内容。
下面我们逐一详细的过一下上面讲到的优化知识点。
三 浏览器缓存
返回目录
浏览器缓存可以简单地理解为 HTTP
缓存。
3.1 缓存位置
返回目录
浏览器缓存位置分 4 个部分:
Service Worker Cache
- 运行在浏览器背后的独立线程。一般可以用来实现缓存功能。Menory Cache
- 内存中的缓存。主要是页面上已经下载的样式、脚本、图片等已经抓取到的资源。Disk Cache
- 硬盘中的缓存。读取速度相对慢点。Push Cache
- 推送缓存。 是 HTTP2 中的内容,当以上 3 种缓存都没有命中的时候,它才会被使用。
3.2 缓存机制
返回目录
- 强缓存
强缓存优先于协商缓存进行,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存。强缓存不会向服务器发送请求,直接从缓存中读取资源。
强缓存利用 HTTP 请求头的 Expires
和 Cache-Control
两个字段来控制。
- 协商缓存
协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器中;生效则返回 304,继续使用缓存。
协商缓存利用 Last-Modified + If-Modified-Since
和 Etag + If-None-Match
来实现。
具体的缓存过程小伙伴们可以看浏览器缓存篇章,这里就不哆嗦了:
四 Cookie、Web Storage 和 IndexDB
返回目录
4.1 Cookie
返回目录
Cookie
最开始被设计出来其实并不是来做本地存储的,而是为了弥补 HTTP
在状态管理上的不足。
Cookie
本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储。
向同一个域名下发送请求,都会携带相同的 Cookie
,服务器拿到 Cookie
进行解析,便能拿到客户端的状态。
缺陷:
- 容量缺陷。体积上线
4kb
,只能存储少量信息。 - 性能缺陷。
Cookie
请求每次都会携带上完整的Cookie
,随着请求数增多,造成性能浪费。 - 安全缺陷。以纯文本的形式在浏览器和服务器中传递,容易被非法截获和篡改。
4.2 Local Storage
返回目录
Local Storge
也是针对同一个域名。
同一个域名下,会存储相同的一段 Local Storage
。
相比 Cookie
优势:
- 容量。体积上线
5M
,大于Cookie
的4kb
。 - 只存在客户端。不参与和服务端的通讯,避免
Cookie
的性能缺陷和安全缺陷。 - 接口封装。有
setItem
和getItem
两个 API 接口。
应用场景:
- 以
Base64
方式存储官方 Logo 等图片。
4.3 Session Storage
返回目录
基本上和 Local Stoarge
一致。
相比较上的不同:
- 会话级别的存储。不同于
Local Storage
的持续化存储,Session Storage
当页面关闭的时候就不复存在了。
应用场景:
- 对表单信息做维护。用户刷新页面不丢失。
- 存储本次浏览记录。看过的页面不怕找不到。
4.4 IndexDB
返回目录
IndexedDB
是运行在浏览器中的 非关系型数据库。
因为本质上是数据库,所以一般来说容量是没有上线的。
五 CDN
返回目录
CDN(Content Delivery Network,内容分发网络)指的是一组分布在各个地区的服务器。
这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。
CDN 提供快速服务,较少受高流量影响。
假设有一部影片出版,非常多人看。jsliang 在广州,请求上海的服务器,结果这个服务器非常多人,资源响应地很慢。于是 jsliang 切换了路线,看到深圳服务器也有这个资源,于是向深圳服务器请求,结果能很快地看到这部影片。
在这个场景中,深圳服务器就扮演 CDN 的角色。
CDN 的核心:缓存 和 回源。
- 缓存:将资源
copy
一份到 CDN 服务器。 - 回源:CDN 发现自己没有这个资源,转头向根服务器(上级服务器)请求这个资源。
应用场景:
- 公司静态资源部署到就近的服务器,利用 CDN 特性方便访问
- jQuery 等框架可以引用 CDN,加快网站的加载速度,避免同一个服务器加载的限制。
- 减少 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
;如果列表越长,或者正确的后缀越往后,尝试的次数就会越多。
所以在配置时为提升构建优化需遵守:
- 频率出现高的文件后缀优先放在前面。
- 列表尽可能的少,例如只有 3 个:
js
、jsx
、json
。 - 书写导入语句时,尽量写上后缀名。
7.2 通过 Loader 和 Plugin 优化
返回目录
7.2.1 babel-loader
返回目录
以 babel-loader
为例,可以通过 include
和 exclude
帮助我们避免 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 浏览器渲染步骤
返回目录
如上图,浏览器的渲染过程为:
- 解析 HTML,生成
DOM
树 - 解析 CSS,生成
CSS 规则树(CSS Rule Tree)
- 将
DOM Tree
和CSS Rule Tree
相结合,生成 渲染树(Render Tree
) - 从根节点开始,计算每一个元素的大小、位置,给出每个节点所应该出现的屏幕精确坐标,从而得到基于渲染树的 布局渲染树(
Layout of the render tree
)。 - 遍历渲染树,将每个节点用 UI 渲染引擎来绘制,从而将整棵树绘制到页面上,这个步骤叫 绘制渲染树(
Painting the render tree
)
11.2 优化 - CSS 选择器问题
返回目录
我们正常的阅读顺序是从左往右的,但是 CSS 解析器解析 CSS 的时候,采用的是古人的规则。
#ul li {}
这样的一行规则,我们写起来的时候很顺畅:先找 id
为 ul
的元素,再找里面的 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 层面的重新像素绘制,因此损耗较少。
什么操作触发回流?
- 添加删除 DOM 元素
- 改变边框、边距、宽高(
border
、margin
、padding
、width
、height
) - 浏览器改变窗口(
resize
) - ……等
什么操作触发重绘?
- 修改背景色、颜色(
background
、color
) - 设置可见度(
visibility
) - 设置背景图(
background-image
) - ……等
我们仔细看这张图,可以看到重排(Layout
)会导致 Render Tree
重构,进而触发重绘(Painting
):
- 回流必定重绘,重绘不一定回流。
因此,我们操作 DOM 的时候,可以这么优化:
- 【CSS】使用
visibility
替换display
- 【CSS】避免
table
布局。对于Render Tree
的计算通常只需要遍历一次就可以完成,但是table
布局需要计算多次,通常要花 3 倍于等同元素的时间,因此要避免。 - 【JS】避免频繁做
width
、height
等会触发回流的操作。 - 【JS】操作 DOM 的时候,如果是添加 DOM 节点,可以将所有节点都在 JS 中操作完毕,再进行渲染(一次性)
十二 预加载页面资源
返回目录
preload
提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。
提供的好处主要是:
- 将加载和执行分离开,可不阻塞渲染和
document
的onload
事件 - 提前加载指定资源,不再出现依赖的
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
标签。
区分 preload
和 prefetch
:
preload
:告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。prefetch
:告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。
当然,开发中需要注意:
- 避免滥用
preload
- 避免混用
preload
和prefetch
- 避免错用
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
并不能实时触发。
实现可见区域渲染的思路:
- 计算当前可见区域起始数据的
startIndex
- 计算当前可见区域结束数据的
endIndex
- 计算当前可见区域的数据,并渲染到页面中
- 计算
startIndex
对应的数据在整个列表中的偏移位置startOffset
,并设置到列表上
代码实现:略。
十四 性能监控
返回目录
- Chrome 工具
Performance
- Chrome 插件
Page Speed
自动化工具
Lighthouse
- Chrome 拓展安装
npm i lighthouse -g
、lighthouse https://www.baidu.com
- Chrome 有基于
LightHouse
的Audits
面板
十五 参考文献
返回目录
本篇参考文献有 31 篇。
15.1 Webpack 优化
返回目录
2019 年文章:
- [x] Webpack优化——将你的构建效率提速翻倍【阅读建议:10min】
- [x] 性能优化篇---Webpack构建速度优化【阅读建议:10min】
- [x] 使用webpack4提升180%编译速度【阅读建议:10min】
- [x] 多进程并行压缩代码【阅读建议:5min】
- [x] webpack 的 scope hoisting 是什么?【阅读建议:5min】
- [x] webpack 4: Code Splitting和chunks切分优化【阅读建议:5min】
2018 年文章:
- [x] Tree-Shaking性能优化实践 - 原理篇【阅读建议:10min】
- [x] 体积减少80%!释放webpack tree-shaking的真正潜力【阅读建议:10min】
- [x] 你的Tree-Shaking并没什么卵用【阅读建议:5min】
- [x] webpack 如何通过作用域分析消除无用代码【阅读建议:5min】
- [x] 让你的Webpack起飞—考拉会员后台Webpack优化实战【阅读建议:5min】
- [x] webpack dllPlugin打包体积和速度优化【阅读建议:5min】
- [x] webpack优化之code splitting【阅读建议:5min】
2017 年文章:
- [x] Webpack 打包优化之速度篇【阅读建议:5min】
- [x] 加速Webpack-缩小文件搜索范围【阅读建议:5min】
- [x] 通过Scope Hoisting优化Webpack输出【阅读建议:5min】
- [x] Webpack 大法之 Code Splitting【阅读建议:5min】
15.2 其他优化
返回目录
- [x] 掘金小册 - 前端性能优化原理与实践【阅读建议:4h】
- [x] 网站性能优化实战——从12.67s到1.06s的故事【阅读建议:30min】
- [x] 聊聊前端开发中的长列表【阅读建议:30min】
- [x] 再谈前端虚拟列表的实现【阅读建议:30min】
- [x] 浅说虚拟列表的实现原理【阅读建议:30min】
- [x] 用 preload 预加载页面资源【阅读建议:20min】
- [x] (译)2019年前端性能优化清单 — 上篇【阅读建议:20min】
- [x] (译)2019年前端性能优化清单 — 中篇【阅读建议:20min】
- [x] (译)2019年前端性能优化清单 — 下篇【阅读建议:20min】
- [x] App内网页启动加速实践:静态资源预加载视角【阅读建议:20min】
- [x] 腾讯HTTPS性能优化实践【阅读建议:30min】
- [x] 5 分钟撸一个前端性能监控工具【阅读建议:20min】
- [x] 现代化懒加载的方式【阅读建议:5min】
- [x] 懒加载和预加载【阅读建议:10min】
jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。<br/>基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/>本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。