前端性能优化原理与实践(三)

摘自前端性能优化原理与实践

DOM 优化原理与基本实践

JS是很快的,在 JS中修改DOM对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM操作并非 JS 一个人的独舞,而是两个模块之间的协作。

JS引擎和渲染引擎(浏览器内核)是独立实现的。当我们用 JS去操作 DOM 时,本质上是JS引擎和渲染引擎之间进行了跨界交流。这个跨界交流的实现并不简单,它依赖了桥接接口作为桥梁

对 DOM 的修改引发样式的更迭

我们对 DOM的操作都不会局限于访问,而是为了修改它。当我们对DOM的修改会引发它外观(样式)上的改变时,就会触发回流或重绘。

这个过程本质上还是因为我们对DOM的修改触发了渲染树(Render Tree)的变化所导致的,重绘不一定导致回流,回流一定会导致重绘。硬要比较的话,回流比重绘做的事情更多,带来的开销也更大。但这两个说到底都是吃性能的,所以都不是什么善茬。我们在开发中,要从代码层面出发,尽可能把回流和重绘的次数最小化。

DOM Fragment

DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的Document使用,用于存储已排好版的或尚未打理好格式的XML片段。因为DocumentFragment不是真实DOM 树的一部分,它的变化不会引起DOM 树的重新渲染的操作(reflow),且不会导致性能等问题。作为脱离了真实DOM树的容器出现,用于缓存批量化的DOM操作。

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)

我们运行这段代码,可以得到与前面两种写法相同的运行结果。
可以看出,DOM Fragment对象允许我们像操作真实 DOM一样去调用各种各样的DOM API,我们的代码质量因此得到了保证。并且它的身份也非常纯粹:当我们试图将其append进真实DOM时,它会在乖乖交出自身缓存的所有后代节点后全身而退,完美地完成一个容器的使命,而不会出现在真实的 DOM结构中。这种结构化、干净利落的特性,使得DOM FragmentDOM Fragment作为经典的性能优化手段大受欢迎。

Event Loop 与异步更新策略

Micro-Task 与 Macro-Task

事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。

  • 常见的 macro-task比如: setTimeoutsetIntervalsetImmediatescript(整体代码)、I/O操作、UI渲染等。
  • 常见的micro-task比如:process.nextTickPromiseMutationObserver等。

Event Loop 过程解析

一个完整的 Event Loop过程,可以概括为以下阶段:

  • 初始状态:调用栈空。micro 队列空,macro队列里有且只有一个 script脚本(整体代码)。

  • 全局上下文(script 标签)被推入调用栈,同步代码执行。在执行的过程中,通过对一些接口的调用,可以产生新的 macro-taskmicro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出macro队列,这个过程本质上是队列的 macro-task的执行和出队的过程。

  • 上一步我们出队的是一个macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task出队时,任务是一个一个执行的;而 micro-task出队时,任务是一队一队执行的(如下图所示)。因此,我们处理micro队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
  • 执行渲染操作,更新界面(敲黑板划重点)。

  • 检查是否存在Web worker任务,如果有,则对其进行处理 。(上述过程循环往复,直到两个队列都清空)

异步更新策略

当我们使用VueReact提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。

最典型的例子,比如有时我们会遇到这样的情况:

// 任务一
this.content = '第一次测试'
// 任务二
this.content = '第二次测试'
// 任务三
this.content = '第三次测试'

我们在三个更新任务中对同一个状态修改了三次,如果我们采取传统的同步更新策略,那么就要操作三次DOM。但本质上需要呈现给用户的目标内容其实只是第三次的结果,也就是说只有第三次的操作是有意义的——我们白白浪费了两次计算。

但如果我们把这三个任务塞进异步更新队列里,它们会先在 JS的层面上被批量执行完毕。当流程走到渲染这一步时,它仅仅需要针对有意义的计算结果操作一次DOM——这就是异步更新的妙处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容