前端性能优化的目的:
- 从用户角度而言:优化能够让页面加载得更快、对用户的操作响应的更及时,能够给用户提供更为良好的体验。(eg:首屏加载时间,页面操作流畅度)
- 从服务商角度而言:优化能够减少页面请求数、或者减小请求所占带宽,能够节省可观的资源。
- 网络请求优化
- 渲染过程优化
- 文件优化
网络请求优化
- CDN加速(原理及使用)
CDN(contentdistribute network,内容分发网络)的本质是一个缓存,将数据缓存在离用户最近的地方,使用户以最快速度获取数据,即所谓网络访问第一跳,如下图。

由于CDN部署在网络运营商的机房,这些运营商又是终端用户的网络服务提供商,因此用户请求路由的第一跳就到达了CDN服务器,当CDN中存在浏览器请求的资源时,从CDN直接返回给浏览器,最短路径返回响应,加快用户访问速度,减少数据中心负载压力。
CDN缓存的一般是静态资源,如图片、文件、CSS、script脚本、静态网页等,但是这些文件访问频度很高,将其缓存在CDN可极大改善网页的打开速度。
- DNS预解析
在浏览器中输入一个域名,回车后,DNS(域名系统)会先将域名解析成对应的IP地址,然后根据IP地址去找到相应的网址。这样就完成了一个DNS查找,这个查找过程当然是要消耗时间的,大约消耗20毫秒,在这个查找过程中,我们的浏览器什么都不会做,保持一片空白。如果这样的查找很多,那么我们的网页性能将会受到很大影响。因此需要DNS缓存(浏览器默认用)和预解析
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.zhix.net">
在一些浏览器的a标签是默认打开dns预解析的,在https协议下dns预解析是关闭的,加入mate后会打开。
- 启用Gzip压缩
通过减小HTTP响应的大小也可以节省HTTP响应时间。Gzip可以压缩所有可能的文件类型,是减少文件体积、增加用户体验的简单方法。
从HTTP/1.1开始,web客户端都默认支持HTTP请求中有Accept-Encoding文件头的压缩格式:Accept-Encoding: gzip
如果web服务器在请求的文件头中检测到上面的代码,就会以客户端列出的方式压缩响应内容。Web服务器把压缩方式通过响应文件头中的Content-Encoding来返回给浏览器。
Content-Encoding: gzip
- 浏览器缓存机制(深入理解浏览器的缓存机制)
浏览器缓存机制

缓存分类

- 传输数据使用protobuf二进制(概念及使用)
GoogleProtocolBuffers简称Protobuf,它提供了一种灵活、高效、自动序列化结构数据的机制,可以联想XML,但是比XML更小、更快、更简单。仅需要自定义一次你所需的数据格式,然后用户就可以使用Protobuf编译器自动生成各种语言的源码,方便的读写用户自定义的格式化的数据。与语言无关,与平台无关,还可以在不破坏原数据格式的基础上,依据老的数据格式,更新现有的数据格式。总的来说就是以下几个特点
- 追求更小,更快,更简洁。
- 与平台无关,与语言无关。
- 前后端需要事先定义数据格式,这点相比较json确实是颇为棘手。
- 解析速度快,比对应的XML快约20-100倍
渲染过程优化
- 减少重排和重绘(概念及方法)
一个完整的渲染流程大致可总结为如下:
-
渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
-
渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
-
创建布局树,并计算元素的布局信息。
-
对布局树进行分层,并生成分层树。
-
为每个图层生成绘制列表,并将其提交到合成线程。
-
合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
-
合成线程发送绘制图块命令DrawQuad给浏览器进程。
-
浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

-
重排:

从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
-
重绘:
接下来,我们再来看看重绘,比如通过 JavaScript 更改某些元素的背景颜色,渲染流水线会怎样调整呢?你可以参考下图:

从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
-
直接合成阶段(动画性能对比):
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图:

在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
- 节流与防抖
防抖函数:短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行,减少http请求;
/**
* @desc 函数防抖
* @param func 目标函数
* @param wait 延迟执行毫秒数
* @param immediate true - 立即执行, false - 延迟执行
*/
function debounce(func, wait, immediate) {
let timer;
return function() {
let context = this,
args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timer = setTimeout(() => {
func.apply
}, wait)
}
}
}
节流函数:只允许一个函数在N秒内执行一次。滚动条调用接口时,可以用节流throttle等优化方式,减少http请求;
/**
* @desc 函数节流
* @param func 函数
* @param wait 延迟执行毫秒数
* @param type 1 表时间戳版,2 表定时器版(其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候)
*/
function throttle(func, wait, type) {
if (type === 1) {
let previous = 0;
} else if (type === 2) {
let timeout;
}
return function() {
let context = this;
let args = arguments;
if (type === 1) {
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
} else if (type === 2) {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}
- js异步加载
<script src="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script async src="script.js"></script>
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步),标记为async的脚本并不保证按照指定它们的先后顺序执行。对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行。。
<script defer src="myscript.js"></script>
有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。关于 defer,我们还要记住的是它是按照加载顺序执行脚本的
// 动态加载js
function loadJS( url, callback ){
var script = document.createElement('script'),
fn = callback || function(){};
script.type = 'text/javascript';
//IE
if(script.readyState){
script.onreadystatechange = function(){
if( script.readyState == 'loaded' || script.readyState == 'complete' ){
script.onreadystatechange = null;
fn();
}
};
}else{
//其他浏览器
script.onload = function(){
fn();
};
}
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
loadJS(myscript.js);
- 使用Web Worker(概念及使用)
Web Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序(反之亦然)。
Web Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。
文件优化(目的也是为了优化文件资源请求)
- 图片延迟加载
在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。
// 一开始设置
<img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4">
// 等页面可见时
const img = document.querySelector('img')
img.src = img.dataset.src
- 响应式图片
响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。
<picture>
<source srcset="banner_w1000.jpg" media="(min-width: 801px)">
<source srcset="banner_w800.jpg" media="(max-width: 800px)">
<img src="banner_w800.jpg" alt="">
</picture>
或
@media (min-width: 769px) {
.bg {
background-image: url(bg1080.jpg);
}
}
@media (max-width: 768px) {
.bg {
background-image: url(bg768.jpg);
}
}
- 降低图片质量
例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。
压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
/*对图片进行压缩*/
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
- import 或require.ensure动态引入组件实现按需加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载
- 压缩文件
在 webpack 可以使用如下插件进行压缩:
- JavaScript:UglifyPlugin
- CSS :MiniCssExtractPlugin
- HTML:HtmlWebpackPlugin
- 提取第三方库
由于引入的第三方库一般都比较稳定,不会经常改变。所以将它们单独提取出来,作为长期缓存是一个更好的选择。
这里需要使用 webpack4 的 splitChunk 插件 cacheGroups 选项
optimization: {
runtimeChunk: {
name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个单独的 chunk。
},
splitChunks: {
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
},
}
},
前端性能优化的目的:
性能优化的几种方向(参考一 参考二 参考三)
网络请求优化
CDN(contentdistribute network,内容分发网络)的本质是一个缓存,将数据缓存在离用户最近的地方,使用户以最快速度获取数据,即所谓网络访问第一跳,如下图。
由于CDN部署在网络运营商的机房,这些运营商又是终端用户的网络服务提供商,因此用户请求路由的第一跳就到达了CDN服务器,当CDN中存在浏览器请求的资源时,从CDN直接返回给浏览器,最短路径返回响应,加快用户访问速度,减少数据中心负载压力。
CDN缓存的一般是静态资源,如图片、文件、CSS、script脚本、静态网页等,但是这些文件访问频度很高,将其缓存在CDN可极大改善网页的打开速度。
在浏览器中输入一个域名,回车后,DNS(域名系统)会先将域名解析成对应的IP地址,然后根据IP地址去找到相应的网址。这样就完成了一个DNS查找,这个查找过程当然是要消耗时间的,大约消耗20毫秒,在这个查找过程中,我们的浏览器什么都不会做,保持一片空白。如果这样的查找很多,那么我们的网页性能将会受到很大影响。因此需要DNS缓存(浏览器默认用)和预解析
在一些浏览器的a标签是默认打开dns预解析的,在https协议下dns预解析是关闭的,加入mate后会打开。
通过减小HTTP响应的大小也可以节省HTTP响应时间。Gzip可以压缩所有可能的文件类型,是减少文件体积、增加用户体验的简单方法。
从HTTP/1.1开始,web客户端都默认支持HTTP请求中有Accept-Encoding文件头的压缩格式:Accept-Encoding: gzip
如果web服务器在请求的文件头中检测到上面的代码,就会以客户端列出的方式压缩响应内容。Web服务器把压缩方式通过响应文件头中的Content-Encoding来返回给浏览器。
Content-Encoding: gzip
浏览器缓存机制
缓存分类
GoogleProtocolBuffers简称Protobuf,它提供了一种灵活、高效、自动序列化结构数据的机制,可以联想XML,但是比XML更小、更快、更简单。仅需要自定义一次你所需的数据格式,然后用户就可以使用Protobuf编译器自动生成各种语言的源码,方便的读写用户自定义的格式化的数据。与语言无关,与平台无关,还可以在不破坏原数据格式的基础上,依据老的数据格式,更新现有的数据格式。总的来说就是以下几个特点
渲染过程优化
一个完整的渲染流程大致可总结为如下:
渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
创建布局树,并计算元素的布局信息。
对布局树进行分层,并生成分层树。
为每个图层生成绘制列表,并将其提交到合成线程。
合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
合成线程发送绘制图块命令DrawQuad给浏览器进程。
浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

重排:

从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
重绘:

接下来,我们再来看看重绘,比如通过 JavaScript 更改某些元素的背景颜色,渲染流水线会怎样调整呢?你可以参考下图:
从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
直接合成阶段(动画性能对比):

那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图:
在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
防抖函数:短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行,减少http请求;
节流函数:只允许一个函数在N秒内执行一次。滚动条调用接口时,可以用节流throttle等优化方式,减少http请求;
Web Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序(反之亦然)。
Web Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。
文件优化(目的也是为了优化文件资源请求)
在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。
响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。
例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。
压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载
在 webpack 可以使用如下插件进行压缩:
由于引入的第三方库一般都比较稳定,不会经常改变。所以将它们单独提取出来,作为长期缓存是一个更好的选择。
这里需要使用 webpack4 的 splitChunk 插件 cacheGroups 选项