性能优化是个老生常谈的话题了,我也是希望通过自己的总结归纳一下自己这么久的学习成果。性能优化这个问题往大了说很复杂,因为可以涉及的面太广了,可以从很多方面入手:比如网络、框架(Vue、React等)、三驾马车(HTML、JS、CSS)等等。由于本人能力有限,如果有错的和不足的地方还希望各位大佬帮忙指正,谢谢。(此篇文章借鉴了前端工匠的文章以及前端性能优化 24 条建议(2020),再此表示感谢。)
现在就从最基本的三驾马车HTML、JS、CSS开始说起。
浏览器渲染过程
现在原生的HTML使用其实越来越少了,大家基本上都是使用三大框架(React、Vue、Angular)来进行操作了。对DOM的操作也越来越少。DOM是渲染引擎的东西,而JS是JS引擎的东西,操作DOM是两个模块的协作来完成的,就好像是两个岛屿中有一个高速公路,一去一来都是收费的,当次数足够多的时候开销就很难被忽略,从而引发性能问题,故而尽量减少DOM操作。
想要了解如何在HTML做优化,我们得先知道浏览器是如何渲染页面的。简单来说,分为以下几个步骤:
HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。- 解析
CSS会产生CSS规则树(CSS Rule Tree),他和DOM结构很类似。 - 等到
JS脚本文件加载以后,通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree。 - 解析完毕以后,浏览器引擎会通过
DOM Tree和CSS Tree来构造渲染树(Rendering Tree),渲染树不等于DOM Tree,渲染树只包含了要显示的节点和这些节点的样式信息,如果某个节点是display:none的,那么就不会在渲染树中显示。 CSS的Rule Tree主要是为了完成匹配并把CSS Rule附加上渲染树(Rendering Tree)上的每个Element。- 计算每个Element的位置,这又叫
layout(布局)和reflow(回流)过程。 - 最后通过调用操作系统
Native GUI的API绘制。
需要注意的是,在遇到JS文件时,会停止渲染,执行JS代码。因为浏览器渲染和JS执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染DOM冲突。原本DOM和CSSOM的构建是互相不影响的,但是由于JS不仅可以更改DOM还可以更改CSSOM。而不完整的CSSOM是无法使用的,JS想访问CSSOM并改变它需要先拿到完整的CSSOM,所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直到其完成了CSSOM的下载和构建。也就是说,在JS要操作CSSOM的情况下,浏览器会先下载和构建CSSOM,然后再执行JS,最后构建DOM。(CSSOM => JS => DOM)
而为了避免JS文件阻塞渲染树的构建,我们可以使用async和defer关键字。
- 绿色线:HTML解析
- 蓝色线:JS加载
- 红色线:JS执行
<script src=“script.js”></script>(没有defer和async):遇到JS文件就先加载JS文件然后马上执行,执行完了以后再解析HTML。<script defer src=“script.js”></script>(延迟执行):异步加载JS文件,加载完成后不会立即执行,而是等到HTML解析完了以后再执行。注意,在有多个JS文件加载时,defer是有序加载的。<script async src=“script.js”></script>(异步下载):同样是异步加载JS文件,加载完了以后就会立刻执行,执行完后再继续HTML的解析。在有多个JS文件加载时,asycn是无序加载的。
- 综上所述:
- 浏览器的工作流程:构建DOM => 构建CSSOM => 构建渲染树 => 布局 => 绘制;
- CSSOM会阻塞渲染,只有当
CSSOM构建完毕以后才会进入下一个阶段构建渲染树; - 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带
defer或者async属性的script标签时,DOM构建将暂停,如果此时又刚好浏览器尚未完成CSSOM的下载和构建,由于JS可以修改CSSOM,所以需要等CSSOM构建完毕以后再执行JS,最后才重新DOM构建。 CSS该放在头部而<script>放在尾部。
回流与重绘
重绘不一定回流,但是回流一定重绘
-
回流(reflow):当我们对DOM修改引发了几何尺寸的变化(修改宽高或者隐藏掉元素)时,浏览器需要重新计算几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来,这个过程就是回流(也叫重排)。常见的回流操作:
- 添加/删除可见的DOM元素
- 元素尺寸改变---边距、填充、边框、宽高
- 内容变化,比如input框中输入文字
- 浏览器窗口尺寸改变
- 计算offerWidth和offsetHeight属性
- 设置style属性的值
-
重绘(repaint):当我们对DOM的修改导致了样式的变化,但是没有影响到几何属性(修改颜色)时,要重新计算元素的几何属性,直接为该元素绘制新的样式。常见的重绘的属性和方法:
- color
- border-style
- box-shadow
- background、background-size、background-image...
优化方案
HTML
- 压缩空白符,减少不必要的注释等字符;
- 尽量不要使用
<iframe>,如果要使用,可以在父页面加载完以后再给<iframe>的src属性赋值,避免<iframe>加载时间过长阻塞主要页面 - 减少不必要的div,避免深层次的嵌套。不然生成DOM树的时候会消耗更大,而且遍历树的时候也会增加开销;
- 不要把节点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
JavaScript
- 在判断条件特别多的时候,建议使用switch语句而不是if-else语句。
- 使用函数节流和函数防抖来减少不必要的接口请求和方法执行。
- 函数节流:在一个单位时间内,只有一次函数回调,多次请求也只有第一次生效
- 应用场景:
1.获取scroll滚动的值
function handleClick(fn,time) { let timer = true return () => { if (!timer) { return } timer = false setTimeout(() => { fn.call(this, arguments) timer = true }, time) } } - 应用场景:
- 函数防抖:在一个单位时间内,只有一次函数回调,多次请求也只有第一次生效
- 应用场景:
1.多次点击某个按钮时
2.input输入内容请求数据时,例如输入搜索
function handleClick(fn,time) { let timer = null return function() { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn.call(this, arguments) }, time) } } - 应用场景:
- 函数节流:在一个单位时间内,只有一次函数回调,多次请求也只有第一次生效
CSS
- 使用
contain属性,可以局部更新样式,可以避免全局的重绘或者回流,减少大量开销; - 使用
visibility:hidden替换display:none。前者只会引发重绘,后者会引发回流; - 不使用
table布局,可能很小的一个小改动都会导致table的重新布局,不过现在应该都是flex布局了; - 随着浏览器的更新,原来讲究CSS选择符应该避免节点层级过多,选择器越短越好,尽量使用ID选择器和类选择器。现在已经不重要了,浏览器在解析的时候已经不会再有这么大的开销了。
- 动画的速度越快,回流次数越多,可以选择使用requestAnimationFrame。
requestAnimationFrame最大的优势就在于两点-
它会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中完成,并且重绘和回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧;
-
隐藏或不可见的元素中,不会进行回流和重绘;
-