Jacleklm's Blog

性能优化

2019/11/06

性能优化概括

主要的是:

  • 降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
  • 加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
  • 缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存localStorage。(HTML 文件每次都向服务器询问是否有更新,JS/CSS/Image资源文件则不请求更新,直接使用本地缓存)
  • 渲染:JS/CSS优化,加载顺序,服务端渲染,pipeline。

加载相关

为什么要强调 CSS 要放在 header 里,js 放在尾部

构建 Render 树需要 DOM 和 CSSOM。由于 CSS 不会阻塞文档的解析,但是会阻塞文档渲染。把 CSS 放在头部可以先生成 CSSOM 树,后续渲染 DOM 的时候,可以一次性构建 Render 树,只需要渲染一次;如果把 CSS 放在后面,会先解析一次 DOM,加载 CSS 之后,会重新渲染之前的 DOM,需要两次渲染。所以 css 放头部能减少渲染时间

而 script 引的 js 文件会阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析 HTML

普通的脚本会阻塞浏览器解析,加上 defer 或 async 属性,脚本就变成异步,可等到解析完毕再执行

  • async 异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在 onload 前,但不确定在 DOMContentLoaded 事件的前后
  • defer 延迟执行,相当于放在 body 最后(理论上在 DOMContentLoaded 事件前)
  • 执行 js 代码过长会卡住渲染,对于需要很多时间计算的代码可以考虑用 Web Worker,它可以让我们另开一个线程执行脚本,不影响渲染

load & DOMContentLoaded & domready

  • DOMContentLoaded 事件触发时,仅当 DOM 加载完成,不包括样式表,图片
  • domready 事件在 DOM 加载后、资源加载之前被触发,在本地浏览器的 DOMContentLoaded 事件的形式被调用
  • load 事件触发时,页面上所有的 DOM,样式表,脚本,图片都已加载完成

白屏、首屏

白屏

白屏时间指的是浏览器开始显示内容的时间,一般认为浏览器开始渲染 body 或者解析完 head 标签的时候就是页面白屏结束的时间
优化

  • 加快 js 的执行速度,比如无限滚动的页面,可以用 js 先渲染一个屏幕范围内的东西
  • 减少文件体积
  • 首屏同步渲染 html,后续的滚屏再异步加载和渲染

首屏

首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间
优化

  • 图片懒加载
  • http 静态资源尽量用多个子域名(能开多个 TCP 链接);
  • 在 js,css,image 等资源响应的 httpheaders 里,设置 expires,last-modified
  • 雪碧图(能减少 HTTP Requests 的数量)

渲染相关

不考虑缓存和优化网络协议的前提下,可以通过哪些方式来最快的渲染页面

无非就是减少生成渲染树的时间了,从 DOM+CSSOM 入手,解决方案如下:

  • 从文件大小考虑
  • 从 script 标签使用上来考虑 async 和 differ
  • 从需要下载的内容是否需要在首屏使用上来考虑
  • CSS 选择器优化,HTML 扁平层级

如何减少重绘回流

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • CSS 选择符从右往左匹配查找,避免节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。通过以下几个常用属性可以生成新图层:
    • position: fixed
    • video、iframe 标签
    • 通过动画实现的 opacity 动画转换

为什么操作 DOM 性能差

DOM 属于渲染引擎中的东西,而 JS 属于 JS 引擎。当我们通过 JS 操作 DOM 的时候,涉及到两个线程之间的通信;并且操作 DOM 可能还会带来重绘回流的情况

如何插入几万个 DOM 并不卡住界面

有两种实现思路:时间分片 和 虚拟列表
详见:高性能渲染十万条数据(时间分片)

window.requestAnimationFrame()

通过 requestAnimationFrame() 去循环的插入 DOM 来。每 16 ms 刷新一次(浏览器是 60Hz 的刷新率,FPS (表示每秒画面更新的次数) 是 60frame/s, 每 1000 / 60 = 16.6ms 才会更新一次)。其实不一定是16ms,因为每个人的屏幕刷新频率不一定都是60,总之 requestAnimationFrame 是跟随系统刷新时间走的,它保证回调函数在屏幕每一次刷新间隔中只能被执行一次,这样就不会引起丢帧的情况
该方法接收一个回调函数作为参数,回调函数会在浏览器下一次重绘之前执行,也可以用来更新动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="container"></ul>
<script>
//需要插入的容器
let ul = document.getElementById('container')
// 插入十万条数据
let total = 100000
// 一次插入 20 条
let once = 20
//总页数
let page = total / once
//每条记录的索引
let index = 0
//循环加载数据
function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false
}
// 每页多少条
let pageCount = Math.min(curTotal, once)
window.requestAnimationFrame(function () {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerText = curIndex + i + ' : ' + Math.floor(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
</script>
</body>
</html>

再优化版:DocumentFragment

MDN

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为 DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题
可以使用 document.createDocumentFragment 方法或者构造函数来创建一个空的 DocumentFragment

所以我们可以认为 DocumentFragment 是存在内存中的,所以将子元素插入到DocumentFragment时不会引起页面回流
所以上面的 loop 函数函数可以改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function loop(curTotal, curIndex) {
if(curTotal <= 0) {
return false
}
let pageCount = Math.min(curTotal, once)
window.requestAnimationFrame(function() {
let fragment = document.createDocumentFragment()
for(let i =0;i < pageCount;i++) {
let li = document.createElement('li')
li.innerText = curIndex + i + ' : ' + (Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount, curIndex + pageCount)
})
}

虚拟滚动

这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。当我们滚动页面的时候就会实时去更新 DOM。react-virtualized 就有这个功能

图片优化

大小优化

计算图片大小:对于一个100px _ 100px的图片,图像中有10000px的点,每一个px4个通道(rgba),每一个通道1个字节(1byte = 8bit),所以该图片大小为10000 _ 4 / 1024` = 39KB

  • 减少像素点
  • 减少每个像素点能够显示的颜色

加载优化

  • 类修饰图片用 CSS 去代替
  • 懒加载
  • 图片都用 CDN 加载。计算出适配屏幕的宽度,然后去请求相应裁剪好的图片
  • 小图使用 base64 格式
  • 将多个图标文件整合到一张图片中(雪碧图)
  • 选择正确的图片格式:
    • 尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量。
    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    • 照片使用 JPEG

防抖节流

防抖

如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。eg. 有一个按钮点击会触发网络请求,但是我们并不希望每次点击都发起网络请求,而是当用户点击按钮一段时间后没有再次点击的情况才去发起网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// func是用户传入需要防抖的函数,wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器
let timer = 0;
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
return function(...args) {
// apply第二个参数是数组
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, wait);
};
};

debounce(str => {
console.log(str);
}, 1000)("666"); // 1s后出现 666

节流

防抖是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。eg. 滚动事件中会发起网络请求,我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// func是用户传入需要节流的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的时间
let lastTime = 0;
return function(...args) {
// 当前时间
let now = new Date();
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now;
func.apply(this, args);
}
};
};

// 测试。setInterval设置了一个每隔50ms执行的,但是节流函数能强制把它改成每隔1000ms执行
setInterval(
throttle(() => {
console.log("我要把你改成1000ms才执行");
}, 1000),
50
);

预加载

可以用rel="preload"开启,强制浏览器请求资源

1
<link rel="preload" href="http://blog.poetries.top">

预渲染

可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染

1
<link rel="prerender" href="http://blog.poetries.top">

预渲染虽然可以提高页面的加载速度,但是要确保该页面大概率会被用户在之后打开,否则就是白白浪费资源去渲染

DNS 预解析

1
<link rel="dns-prefetch" href="//blog.poetries.top">

懒执行

懒执行就是将某些逻辑延迟到使用时再计算
该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行
懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒

懒加载

懒加载就是将不关键的资源延后加载
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西

对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中。当进入视口区时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<img
src=""
data-src="https://clubimg.club.vmall.com/data/attachment/forum/201911/10/101520zmot0v4cy3r1movs.jpg"
alt=""
/>
<!-- 有若干图片 -->
<script>
let img = document.querySelectorAll("img");
let n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历
lazyload(); //页面载入完毕加载可是区域内的图片
window.onscroll = lazyload;
function lazyload() {
//监听页面滚动事件
let clientHeight = document.documentElement.clientHeight; //可见区域高度
let scrollTop =
document.documentElement.scrollTop || document.body.scrollTop; //滚动条距离顶部高度
for (let i = n; i < img.length; i++) {
// 图片未出现时距离顶部的距离大于滚动条距顶部的距离+可视区的高度
if (img[i].offsetTop < clientHeight + scrollTop) {
img[i].src = img[i].getAttribute("data-src");
n = i + 1;
}
}
}
</script>

懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等

CDN

CDN(Content Delivery Network,内容分发网络)将源站的内容发布到接近用户的网络“边缘”,用户可以就近获取所需数据(缓存数据),不仅降低了网络的拥塞状况、提高请求的响应速度,也能够减少源站的负载压力。
因此,我们可以将静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。并且对于 CDN 加载静态资源需要注意CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie,平白消耗流量

参考资料
掘金小册-前端面试之道

CDN的原理

参考 博客1博客2

CATALOG
  1. 1. 性能优化概括
  2. 2. 加载相关
    1. 2.1. 为什么要强调 CSS 要放在 header 里,js 放在尾部
    2. 2.2. load & DOMContentLoaded & domready
    3. 2.3. 白屏、首屏
      1. 2.3.1. 白屏
      2. 2.3.2. 首屏
  3. 3. 渲染相关
    1. 3.1. 不考虑缓存和优化网络协议的前提下,可以通过哪些方式来最快的渲染页面
    2. 3.2. 如何减少重绘回流
    3. 3.3. 为什么操作 DOM 性能差
    4. 3.4. 如何插入几万个 DOM 并不卡住界面
      1. 3.4.1. window.requestAnimationFrame()
      2. 3.4.2. 再优化版:DocumentFragment
      3. 3.4.3. 虚拟滚动
  4. 4. 图片优化
    1. 4.1. 大小优化
    2. 4.2. 加载优化
  5. 5. 防抖节流
    1. 5.1. 防抖
    2. 5.2. 节流
  6. 6. 预加载
  7. 7. 预渲染
  8. 8. DNS 预解析
  9. 9. 懒执行
  10. 10. 懒加载
  11. 11. CDN
    1. 11.1. CDN的原理