笔记--前端性能优化--渲染篇下

167 阅读5分钟

建议购买原文,前端性能优化原理与实践,本文学习总结用。如有侵权,感谢联系,删除。

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)大点,下的“最容易被忽略的操作”。