建议购买原文,前端性能优化原理与实践,本文学习总结用。如有侵权,感谢联系,删除。
1、渲染篇
1)、 DOM 为什么这么慢
JS引擎和渲染引擎,两个线程之间的操作很耗性能,也会导致回流重绘问题
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。 —— 《高性能 JavaScript》
2)、 解决方案一,给你的 DOM “提提速”,
- 减少 DOM 操作:少交“过路费”、避免过度渲染,js去给dom分压
//需求
for(var count=0;count<10000;count++){
document.getElementById('container').innerHTML+='<span>我是⼀一个⼩小测试</span>'
}
//代码优化后:
//变量缓存;合并,一次性操作。
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){
// 先对内容进⾏行行操作
content += '<span>我是⼀一个⼩小测试</span>'
}
// 内容处理理好了了,最后再触发DOM的更更改
container.innerHTML = content
//优雅优化,DOM Fragment
let container = document.getElementById('container')
// 创建⼀一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是⼀一个⼩小测试'
// 像操作真实DOM⼀一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理理好了了,最后再触发真实DOM的更更改
container.appendChild(content)
3)、解决方案二,Event Loop 与异步更新策略
①、Micro-Task 与 Macro-Task
事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。
- 常见的 macro-task 比如: setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操 作、UI 渲染等。
- 常见的 micro-task 比如: process.nextTick、Promise、MutationObserver 等。
一个完整的 Event Loop 过程:
micro 队列空,macro 队列里有且只有一个 script 脚本--
-->全局上下文(script 标签)被推入调用栈,同步代码执行
-->上一步我们出队的是一个macro-task,这一步我们处理的是micro-task
-->执行渲染操作,更新界面(敲黑板划重点)
-->检查是否存在 Web worker 任务,如果有,则对其进行处理 。
(上述过程循环往复,直到两个队列都清空)
Micro-Task出队是一队队出,Macro-Task出队是一个个出
setTimeout(task, 0)//放入macro,当执行渲染操作完,还没有更新(因为这次顺序是script,micro,render,setTimeout)
Promise.resolve().then(task)//放入micro,当执行渲染操作前,更新了
因此,我们更新 DOM 的时间点,应该尽可能靠近渲染的时机。当我们需要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。
②、异步更新策略——以 Vue 为例
当我们使用 Vue 或 React 提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到 一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。
优点
- 只看结果,因此渲染引擎不需要为过程买单。
4)、解决方案三,回流(Reflow)与重绘 (Repaint)
回流导致的原因:
- 最“贵”的操作:改变 DOM 元素的几何属性,常见的几何属性有 width、height、padding、margin、left、top、border 等等
- “价格适中”的操作:改变 DOM 树的结构,这里主要指的是节点的增减、移动等操作。
- 最容易被忽略的操作:获取一些特定属性的值
当你要用到像这样的属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、 scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,你就 要注意了! 要通过即时计算,浏览器为了获取这些值,会进行回流
除此之外,当我们调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。 原理是一样的,都为求一个 “即时性” 和 “准确性”
规避回流与重绘
- 将“导火索”缓存起来,避免频繁改动
// 获取el元素
const el = document.getElementById('el')
// 这⾥里里循环判定⽐比较简单,实际中或许会拓拓展出⽐比较复杂的判定需求
for(let i=0;i<10;i++) {
el.style.top = el.offsetTop + 10 + "px";
el.style.left = el.offsetLeft + 10 + "px";
}
//优化
// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el')
let offLeft = el.offsetLeft,offTop = el.offsetTop
// 在JS层⾯面进⾏行行计算
for(let i=0;i<10;i++) {
offLeft += 10 offTop += 10
}
// ⼀一次性将计算结果应⽤用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop + "px"
- 避免逐条改变样式,使用类名去合并样式
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
//优化
// css
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
const container = document.getElementById('container')
container.classList.add('basic_style')
- 将 DOM “离线”
let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red' ...(省略略了了许多类似的后续操作)
container.style.display = 'block'
当我们只需 要进行很少的 DOM 操作时,DOM 离线化的优越性确实不太明显。一旦操作频繁起来,这“拿 掉”和“放回”的开销都将会是非常值得的。
5)、Flush 队列:浏览器并没有那么简单
浏览器自己缓存了一个 flush 队列,把我们触发的回流与重绘任 务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将 这些任务一口气出队。
“不得已”:指的是当获取很强的“即时性”的属性值时。浏览器会为了获得此时此刻的、最准 确的属性值,而提前将 flush 队列的任务出队——这就是所谓的“不得已”时刻。具体是哪些属性 值,在4)大点,下的“最容易被忽略的操作”。