前端性能优化 23 条建议(2020)

性能优化是把双刃剑,有好的一面也有坏的一面。好的一面就是能提高网站性能,坏的一面就是配置麻烦,或者要遵照的规则太多。而且某些性能优化规则并不适用全部场景,须要谨慎使用,请读者带着批判性的眼光来阅读本文。javascript

本文相关的优化建议的引用资料出处均会在建议后面给出,或者放在文末(有些参考资料可能要梯子才能观看)。css

1. 减小 HTTP 请求

一个 HTTP 请求过程:
在这里插入图片描述
一个 HTTP 请求须要经历以上过程,接下来看一个具体的例子:
在这里插入图片描述
这是一个 HTTP 请求,请求的文件大小为 28.4KB。html

名词解释:前端

  • Queueing: 在请求队列中的时间。
  • Stalled: 从TCP 链接创建完成,到真正能够传输数据之间的时间差,此时间包括代理协商时间。
  • Proxy negotiation: 与代理服务器链接进行协商所花费的时间。
  • DNS Lookup: 执行DNS查找所花费的时间,页面上的每一个不一样的域都须要进行DNS查找。
  • Initial Connection / Connecting: 创建链接所花费的时间,包括TCP握手/重试和协商SSL。
  • SSL: 完成SSL握手所花费的时间。
  • Request sent: 发出网络请求所花费的时间,一般为一毫秒的时间。
  • Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间总和,它包含了 DNS 解析时间、 TCP 链接时间、发送 HTTP 请求时间和得到响应消息第一个字节的时间。
  • Content Download: 接收响应数据所花费的时间。

从这个例子能够看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为何要建议将多个小文件合并为一个大文件,从而减小 HTTP 请求次数的缘由。vue

参考资料:java

2. 使用 HTTP2

HTTP1.x 客户端须要使用多个链接才能实现并发和缩短延迟;HTTP1.x 不会压缩请求和响应标头,从而致使没必要要的网络流量;HTTP1.x 不支持有效的资源优先级,导致底层 TCP 链接的利用率低下等等。

HTTP2 是对以前 HTTP 标准的扩展,它经过支持标头字段压缩和在同一链接上进行多个并发交换,让应用更有效地利用网络资源,减小感知的延迟时间。具体来讲,它能够对同一链接上的请求和响应消息进行交错发送并为 HTTP 标头字段使用有效编码。node

HTTP2 还容许为请求设置优先级,让更重要的请求更快速地完成,从而进一步提高性能。webpack

HTTP2 支持了多路复用,HTTP 链接变得十分廉价,以前为了节省链接数所采用的相似于「资源合并、资源内联」等优化手段再也不须要了。多路复用能够在一个 TCP 链接上创建大量 HTTP 链接,也就不存在 HTTP 链接数限制了,HTTP1.x 中常见的「静态域名」优化策略不但用不上了,还会带来负面影响,须要去掉。另外,HTTP2 的头部压缩功能也能大幅减小 HTTP 协议头部带来的开销。可是,要等HTTP1.x 彻底退出舞台还须要一段时间。git

如今有不少网站已经开始使用 HTTP2 了,例如知乎:
在这里插入图片描述
其中 h2 是指 HTTP2 协议,http/1.1 则是指 HTTP1.1 协议。github

参考资料:

3. 使用服务端渲染

客户端渲染: 获取 HTML 文件,根据须要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。
服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。

  • 优势:首屏渲染快,SEO 好。
  • 缺点:配置麻烦。

参考资料:

4. 静态资源使用 CDN

内容分发网络(CDN)是一组分布在多个不一样地理位置的 Web 服务器。咱们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。

参考资料:

5. 将 CSS 放在文件头部,JavaScript 文件放在底部

全部放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。若是这些 CSS 和 JS 须要加载和解析好久的话,那么页面就空白了。因此 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。

那为何 CSS 文件还要放在头部呢?

由于先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了不这种状况发生,就要将 CSS 文件放在头部了。

另外,JS 文件也不是不能够放在头部,只要给 script 标签加上 defer 属性就能够了,异步下载,延迟执行。

6. 使用字体图标 iconfont 代替图片图标

字体图标就是将图标制做成一个字体,使用时就跟字体同样,能够设置属性,例如 font-size、color 等等,很是方便。而且字体图标是矢量图,不会失真。还有一个优势是生成的文件特别小。

7. 善用缓存,不重复加载相同的资源

为了不用户每次访问网站都得请求文件,咱们能够经过添加 Expires 头来控制这一行为。Expires 设置了一个时间,只要在这个时间以前,浏览器都不会请求文件,而是直接使用缓存。

不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器从新请求文件?

能够经过更新页面中引用的 CSS 连接地址,让浏览器主动放弃缓存,加载新资源。

具体作法是把 CSS 地址的修改与文件内容关联起来,也就是说,只有文件内容变化,才会致使相应 CSS 地址的变动,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?咱们会很天然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种能够精确到单个文件粒度的缓存控制依据了。

参考资料:

8. 压缩文件

压缩文件能够减小文件下载时间,让用户体验性更好。

得益于 webpack 和 node 的发展,如今压缩文件已经很是方便了。

在 webpack 可使用以下插件进行压缩:

  • JavaScript:UglifyPlugin
  • CSS :MiniCssExtractPlugin
  • HTML:HtmlWebpackPlugin

其实,咱们还能够作得更好。那就是使用 gzip 压缩。能够经过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。固然,服务器也得支持这一功能。

gzip 是目前最流行和最有效的压缩方法。举个例子,我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减小了将近 60%。

附上 webpack 和 node 配置 gzip 的使用方法。

下载插件

npm install compression-webpack-plugin --save-dev
npm install compression

webpack 配置

const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [new CompressionPlugin()],
}

node 配置

const compression = require('compression')
// 在其余中间件前使用
app.use(compression())

9. 图片优化

(1). 图片延迟加载
在页面中,先不给图片设置路径,只有当图片出如今浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片不少的网站来讲,一次性加载所有图片,会对用户体验形成很大的影响,因此须要使用图片延迟加载。

首先能够将图片这样设置,在页面不可见时图片不会加载:

<img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4">

等页面可见时,使用 JS 加载图片:

const img = document.querySelector('img')
img.src = img.dataset.src

这样图片就加载出来了,完整的代码能够看一下参考资料。

参考资料:

(2). 响应式图片
响应式图片的优势是浏览器可以根据屏幕大小自动加载合适的图片。

经过 picture 实现

<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 实现

@media (min-width: 769px) {
    .bg {
        background-image: url(bg1080.jpg);
    }
}
@media (max-width: 768px) {
    .bg {
        background-image: url(bg768.jpg);
    }
}

(3). 调整图片大小
例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展现给用户,而且当用户鼠标悬停在上面时才展现全图。若是用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。

因此,咱们能够用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在全部元素都加载完成后手动更改大图的 src 进行下载。

(4). 下降图片质量
例如 JPG 格式的图片,100% 的质量和 90% 质量的一般看不出来区别,尤为是用来当背景图的时候。我常常用 PS 切背景图时, 将图片切成 JPG 格式,而且将它压缩到 60% 的质量,基本上看不出来区别。

除此以外,网上还有不少在线压缩图片的网站,你们能够自行搜索。

(5). 尽量利用 CSS3 效果代替图片
有不少图片使用 CSS 效果(渐变、阴影等)就能画出来,这种状况选择 CSS3 效果更好。由于代码大小一般是图片大小的几分之一甚至几十分之一。

10. 经过 webpack 按需加载 JavaScript 代码

懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式其实是先把你的代码在一些逻辑断点处分离开,而后在一些代码块中完成某些操做后,当即引用或即将引用另一些新的代码块。这样加快了应用的初始加载速度,减轻了它的整体体积,由于某些代码块可能永远不会被加载。

若是你使用脚手架来构建项目,通常配置起来很是简单,具体细节可看一下 webpack 文档。

参考资料:

11. 减小重绘重排

浏览器渲染过程

  1. 解析HTML生成DOM树。
  2. 解析CSS生成CSSOM规则树。
  3. 将DOM树与CSSOM规则树合并在一块儿生成渲染树。
  4. 遍历渲染树开始布局,计算每一个节点的位置大小信息。
  5. 将渲染树每一个节点绘制到屏幕。

在这里插入图片描述

重排
当改变 DOM 元素位置或大小时,会致使浏览器从新生成渲染树,这个过程叫重排。

重绘
当从新生成渲染树后,就要将渲染树每一个节点绘制到屏幕,这个过程叫重绘。不是全部的动做都会致使重排,例如改变字体颜色,只会致使重绘。记住,重排会致使重绘,重绘不会致使重排 。

重排和重绘这两个操做都是很是昂贵的,由于 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工做。

什么操做会致使重排?

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变
  • 内容改变
  • 浏览器窗口尺寸改变

如何减小重排重绘?

  • 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 若是要对 DOM 元素执行一系列操做,能够将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。

12. 使用事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就能够管理某一类型的全部事件。全部用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托能够节省内存。

<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>凤梨</li>
</ul>

// good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
})

13. 注意程序的局部性

一个编写良好的计算机程序经常具备良好的局部性,它们倾向于引用最近引用过的数据项附近的数据项,或者最近引用过的数据项自己,这种倾向性,被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。

局部性一般有两种不一样的形式:

  • 时间局部性:在一个具备良好时间局部性的程序中,被引用过一次的内存位置极可能在不远的未来被屡次引用。
  • 空间局部性 :在一个具备良好空间局部性的程序中,若是一个内存位置被引用了一次,那么程序极可能在不远的未来引用附近的一个内存位置。

时间局部性示例

function sum(arry) {
    let i, sum = 0
    let len = arry.length

    for (i = 0; i < len; i++) {
        sum += arry[i]
    }

    return sum
}

在这个例子中,变量sum在每次循环迭代中被引用一次,所以,对于sum来讲,具备良好的时间局部性

空间局部性示例

具备良好空间局部性的程序

// 二维数组 
function sum1(arry, rows, cols) {
    let i, j, sum = 0

    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            sum += arry[i][j]
        }
    }
    return sum
}

空间局部性差的程序

// 二维数组 
function sum2(arry, rows, cols) {
    let i, j, sum = 0

    for (j = 0; j < cols; j++) {
        for (i = 0; i < rows; i++) {
            sum += arry[i][j]
        }
    }
    return sum
}

看一下上面的两个空间局部性示例,像示例中从每行开始按顺序访问数组每一个元素的方式,称为具备步长为1的引用模式。
若是在数组中,每隔k个元素进行访问,就称为步长为k的引用模式。
通常而言,随着步长的增长,空间局部性降低。

这两个例子有什么区别?区别在于第一个示例是按行扫描数组,每扫描完一行再去扫下一行;第二个示例是按列来扫描数组,扫完一行中的一个元素,立刻就去扫下一行中的同一列元素。

数组在内存中是按照行顺序来存放的,结果就是逐行扫描数组的示例获得了步长为 1 引用模式,具备良好的空间局部性;而另外一个示例步长为 rows,空间局部性极差。

性能测试
运行环境:

  • cpu: i5-7400
  • 浏览器: chrome 70.0.3538.110

对一个长度为9000的二维数组(子数组长度也为9000)进行10次空间局部性测试,时间(毫秒)取平均值,结果以下:

所用示例为上述两个空间局部性示例

步长为 1 步长为 9000
124 2316

从以上测试结果来看,步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。

总结:

  • 重复引用相同变量的程序具备良好的时间局部性
  • 对于具备步长为 k 的引用模式的程序,步长越小,空间局部性越好;而在内存中以大步长跳来跳去的程序空间局部性会不好

参考资料:

14. if-else 对比 switch

当判断条件数量愈来愈多时,越倾向于使用 switch 而不是 if-else。

if (color == 'blue') {

} else if (color == 'yellow') {

} else if (color == 'white') {

} else if (color == 'black') {

} else if (color == 'green') {

} else if (color == 'orange') {

} else if (color == 'pink') {

}

switch (color) {
    case 'blue':

        break
    case 'yellow':

        break
    case 'white':

        break
    case 'black':

        break
    case 'green':

        break
    case 'orange':

        break
    case 'pink':

        break
}

像以上这种状况,使用 switch 是最好的。假设 color 的值为 pink,则 if-else 语句要进行 7 次判断,switch 只须要进行一次判断。
从可读性来讲,switch 语句也更好。从使用时机来讲,当条件值大于两个的时候,使用 switch 更好。

不过,switch 只能用于 case 值为常量的分支结构,而 if-else 更加灵活。

15. 查找表

当条件语句特别多时,使用 switch 和 if-else 不是最佳的选择,这时不妨试一下查找表。查找表可使用数组和对象来构建。

switch (index) {
    case '0':
        return result0
    case '1':
        return result1
    case '2':
        return result2
    case '3':
        return result3
    case '4':
        return result4
    case '5':
        return result5
    case '6':
        return result6
    case '7':
        return result7
    case '8':
        return result8
    case '9':
        return result9
    case '10':
        return result10
    case '11':
        return result11
}

能够将这个 switch 语句转换为查找表

const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]

return results[index]

若是条件语句不是数值而是字符串,能够用对象来创建查找表

const map = {
  red: result0,
  green: result1,
}

return map[color]

16. 避免页面卡顿

60fps 与设备刷新率

目前大多数设备的屏幕刷新率为 60 次/秒。所以,若是在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也须要跟设备屏幕的刷新率保持一致。
其中每一个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工做要作,所以您的全部工做须要在 10 毫秒内完成。若是没法符合此预算,帧率将降低,而且内容会在屏幕上抖动。 此现象一般称为卡顿,会对用户体验产生负面影响。

在这里插入图片描述
假如你用 JavaScript 修改了 DOM,并触发样式修改,经历重排重绘最后画到屏幕上。若是这其中任意一项的执行时间过长,都会致使渲染这一帧的时间过长,平均帧率就会降低。假设这一帧花了 50 ms,那么此时的帧率为 1s / 50ms = 20fps,页面看起来就像卡顿了同样。

对于一些长时间运行的 JavaScript,咱们可使用定时器进行切分,延迟执行。

for (let i = 0, len = arry.length; i < len; i++) {
    process(arry[i])
}

假设上面的循环结构因为 process() 复杂度太高或数组元素太多,甚至二者都有,能够尝试一下切分。

const todo = arry.concat()
setTimeout(() => {
    process(todo.shift())
    if (todo.length) {
        setTimeout(arguments.callee, 25)
    } else {
        callback(arry)
    }
}, 25)

若是有兴趣了解更多,能够查看一下高性能JavaScript第 6 章和高效前端:Web高效编程与优化实践第 3 章。

参考资料:

17. 使用 requestAnimationFrame 来实现视觉变化

从第 16 点咱们能够知道,大多数设备屏幕刷新率为 60 次/秒,也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候,最好的状况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的惟一方式是使用 requestAnimationFrame

/**
 * If run as a requestAnimationFrame callback, this
 * will be run at the start of the frame.
 */
function updateScreen(time) {
  // Make visual updates here.
}

requestAnimationFrame(updateScreen);

若是采起 setTimeoutsetInterval 来实现动画的话,回调函数将在帧中的某个时点运行,可能恰好在末尾,而这可能常常会使咱们丢失帧,致使卡顿。
在这里插入图片描述
参考资料:

18. 使用 Web Workers

Web Worker 使用其余工做线程从而独立于主线程以外,它能够执行任务而不干扰用户界面。一个 worker 能够将消息发送到建立它的 JavaScript 代码, 经过将消息发送到该代码指定的事件处理程序(反之亦然)。

Web Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。

建立一个新的 worker 很简单,指定一个脚本的 URI 来执行 worker 线程(main.js):

var myWorker = new Worker('worker.js');
// 你能够经过postMessage() 方法和onmessage事件向worker发送消息。
first.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

second.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

在 worker 中接收到消息后,咱们能够写一个事件处理函数代码做为响应(worker.js):

onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}

onmessage处理函数在接收到消息后立刻执行,代码中消息自己做为事件的data属性进行使用。这里咱们简单的对这2个数字做乘法处理并再次使用postMessage()方法,将结果回传给主线程。

回到主线程,咱们再次使用onmessage以响应worker回传的消息:

myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}

在这里咱们获取消息事件的data,而且将它设置为result的textContent,因此用户能够直接看到运算的结果。

不过在worker内,不能直接操做DOM节点,也不能使用window对象的默认方法和属性。然而你可使用大量window对象之下的东西,包括WebSockets,IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。

参考资料:

19. 使用位操做

JavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。可是在位操做中,数字被转换为有符号的 32 位格式。即便须要转换,位操做也比其余数学运算和布尔操做快得多。
取模
因为偶数的最低位为 0,奇数为 1,因此取模运算能够用位操做来代替。

if (value % 2) {
    // 奇数
} else {
    // 偶数 
}
// 位操做
if (value & 1) {
    // 奇数
} else {
    // 偶数
}

取反

~~10.12 // 10
~~10 // 10
~~'1.5' // 1
~~undefined // 0
~~null // 0

位掩码

const a = 1
const b = 2
const c = 4
const options = a | b | c

经过定义这些选项,能够用按位与操做来判断 a/b/c 是否在 options 中。

// 选项 b 是否在选项中
if (b & options) {
    ...
}

20. 不要覆盖原生方法

不管你的 JavaScript 代码如何优化,都比不上原生方法。由于原生方法是用低级语言写的(C/C++),而且被编译成机器码,成为浏览器的一部分。当原生方法可用时,尽可能使用它们,特别是数学运算和 DOM 操做。

21. 下降 CSS 选择器的复杂性

(1). 浏览器读取选择器,遵循的原则是从选择器的右边到左边读取。

看个示例

#block .text p {
    color: red;
}
  1. 查找全部 P 元素。
  2. 查找结果 1 中的元素是否有类名为 text 的父元素
  3. 查找结果 2 中的元素是否有 id 为 block 的父元素

(2). CSS 选择器优先级

内联 > ID选择器 > 类选择器 > 标签选择器

根据以上两个信息能够得出结论。

  1. 选择器越短越好。
  2. 尽可能使用高优先级的选择器,例如 ID 和类选择器。
  3. 避免使用通配符 *。

最后要说一句,据我查找的资料所得,CSS 选择器没有优化的必要,由于最慢和慢快的选择器性能差异很是小。

参考资料:

22. 使用 flexbox 而不是较早的布局模型

在早期的 CSS 布局方式中咱们能对元素实行绝对定位、相对定位或浮动定位。而如今,咱们有了新的布局方式 flexbox,它比起早期的布局方式来讲有个优点,那就是性能比较好。

下面的截图显示了在 1300 个框上使用浮动的布局开销:
在这里插入图片描述
而后咱们用 flexbox 来重现这个例子:
在这里插入图片描述
如今,对于相同数量的元素和相同的视觉外观,布局的时间要少得多(本例中为分别 3.5 毫秒和 14 毫秒)。

不过 flexbox 兼容性仍是有点问题,不是全部浏览器都支持它,因此要谨慎使用。

各浏览器兼容性:

  • Chrome 29+
  • Firefox 28+
  • Internet Explorer 11
  • Opera 17+
  • Safari 6.1+ (prefixed with -webkit-)
  • Android 4.4+
  • iOS 7.1+ (prefixed with -webkit-)

参考资料:

23. 使用 transform 和 opacity 属性更改来实现动画

在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是能够由合成器(composite)单独处理的属性。
在这里插入图片描述

参考资料:

其余参考资料

更多文章,欢迎关注