脚本执行时浏览器重排触发机制及批量DOM插入性能问题
关于浏览器重排(reflow)触发时机与DOM插入性能问题解答
一、重排的触发时机
浏览器的JS执行和UI渲染是互斥的,跑在同一个主线程上。默认情况下,浏览器会把当前同步执行的JS代码段里所有的DOM修改操作缓存起来,等到当前JS执行栈完全清空之后,才会统一执行一次布局计算、重排重绘,不会每改一次DOM就触发一次重排。
例外情况:如果在两次DOM修改之间,你主动读取了和布局相关的属性(比如
offsetWidth、clientTop、getBoundingClientRect()等),浏览器为了返回最新的准确值,会强制立即同步触发重排,这种情况就会出现你说的“每次插入都暂停脚本执行计算布局”的问题。
二、两段代码的性能差异
两段代码存在非常明显的性能差异,元素数量越多差距越大:
第一段代码(循环插入body)
for(let i = 0; i < 100; i++){ $('body').append('<div>SOmething....</div>') }
- 每次
append操作的对象是已经挂载在页面上的body(属于活DOM树的一部分),就算没有触发强制同步重排,每次操作活DOM本身也会带来额外的性能开销 - 如果后续代码中不小心插入了布局属性读取操作,会直接触发100次重排,性能会暴跌
第二段代码(离线操作后统一插入)
let a = $('<div></div>'); for(let i = 0; i < 100; i++){ $(a).append('<div>SOmething....</div>') } $('body').append(a)
- 循环内的所有
append操作都是在未挂载到活DOM树的离线节点上执行的,这个过程完全不会触发任何重排,操作成本极低 - 最后只执行1次活DOM插入操作,最多触发1次重排,性能远高于第一段代码
三、超大量元素插入的优化方案
如果要插入10000个甚至更多元素,可以用以下方案避免卡顿:
- 使用DocumentFragment:相比创建离线div,DocumentFragment是专门用来批量存储DOM节点的容器,它本身不会产生额外的DOM节点,插入到页面时只会把它的所有子节点插入进去,不会多一层冗余的父节点,性能更高
示例代码:const fragment = document.createDocumentFragment() for(let i = 0; i < 10000; i++){ const div = document.createElement('div') div.textContent = 'Something....' fragment.appendChild(div) } document.body.appendChild(fragment) - 隐藏父元素后操作:先把要插入的目标父元素设置
display: none,所有DOM操作完成后再恢复显示,隐藏状态下的DOM修改完全不会触发重排 - innerHTML批量拼接:先把所有要插入的HTML拼接成一个完整字符串,最后一次性赋值给父元素的
innerHTML,性能比多次节点操作更高,注意如果内容是用户输入的要做好XSS防护 - 分块懒插入:用
requestAnimationFrame把插入操作拆分到不同帧执行,比如每帧插入200个节点,避免单帧执行时间过长阻塞主线程导致页面卡顿 - 虚拟滚动:如果是长列表场景,最推荐的方案是只渲染用户可视区域内的节点,非可视区域的节点不插入DOM,不管是10万还是百万级别的数据都不会出现卡顿
内容的提问来源于stack exchange,提问作者B. Ma
相关产品推荐
相关产品推荐

