想象你开了一家叫「前端小馆」的餐厅。顾客在浏览器地址栏敲下一个网址,就像把一张手写菜单递进后厨,这张菜单就是 HTML。后厨拿到菜单后要依次完成五道工序:先读懂字迹(解析),再对照主厨手册决定每道菜的做法(样式计算),接着计算每张桌子的位置和间距(布局),然后铺桌布、摆餐具、做装饰(绘制),最后把菜端上桌(合成)。这整套流程,就是浏览器的「渲染管线」。在这家餐厅里,老板最怕两件事:一是客人临时要挪桌子,后厨得把整排餐桌重新排一遍,这叫「回流」;二是客人嫌桌布颜色不好看,后厨只需换一块桌布,这叫「重绘」。回流贵、重绘便宜——理解了这点,你就握住了页面性能优化的钥匙。这篇文章,就带你跟着这张菜单,把后厨的每一步都走一遍。

岗位关联

掌握渲染管线与重绘回流优化,是前端性能工程师的立身之本:能把「页面卡顿」从玄学变成可定位、可衡量的工程问题,是写出 60fps 流畅体验的底层能力。

目录
  1. 可视化演示
  2. 渲染流水线总览
  3. 解析HTML建DOM
  4. CSSOM样式计算
  5. 布局与回流
  6. 绘制与重绘
  7. 合成与分层
  8. 性能优化策略
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示浏览器将HTML/CSS/JS解析为像素的Pars→Styl→Layt→Pnt→Comp五阶段渲染流水线,标注Reflow和Repaint的触发范围与优化方向。

本节配有交互动效(进入视口后自动加载,约 3KB)。

浏览器渲染流水线 Pars→Styl→Layt→Pnt→Comp五阶段,HTML/CSS/JS逐步转换为屏幕像素 Reflow(重排)触发完整重排链路,Repaint(重绘)仅触发Pnt+Comp阶段 优化:减少Reflow → 批量DOM操作 → GPU加速

渲染流水线总览

在「前端小馆」里,顾客点完菜后,后厨并不是一口气把菜端上来,而是分成五个岗位接力:跑单员先看清菜单(解析),配菜师按主厨手册定做法(样式计算),摆桌员计算每张桌子的位置和大小(布局),摆盘师把餐盘装饰得漂亮(绘制),最后传菜员把成品端上桌(合成)。五个岗位各司其职,谁也不能跳过谁。

浏览器的工作与此如出一辙。它把一段 HTML/CSS/JS 变成屏幕上的像素,要依次经过「解析、样式计算、布局、绘制、合成」五个阶段。JavaScript 还会在任意阶段插队,修改输入数据,逼着后面的岗位重新干活。这也是为什么同样一个页面,有的设备行云流水,有的设备卡成幻灯片——差异往往就出在这条管线走得顺不顺。

这五个阶段合起来,专业上叫「关键渲染路径」(Critical Rendering Path)。页面首次打开时,路径越短,用户看到内容越早;交互时,任何一次数据更新都会重新走这条路上的某几段。所以评估性能,本质上就是计算这条路的总耗时,再决定砍掉哪一段。

理解了这条流水线,再看「回流」和「重绘」就很容易:它们分别是布局和绘制这两个岗位被迫返工。接下来,我们逐个岗位参观。

T渲染=T解析+T样式+T布局+T绘制+T合成T_{\text{渲染}} = T_{\text{解析}} + T_{\text{样式}} + T_{\text{布局}} + T_{\text{绘制}} + T_{\text{合成}}
公式解读:页面渲染总耗时约等于五个阶段耗时的总和。想优化总时间,要么压缩每一段,要么让某些段压根不执行。
  1. T_渲染页面从 HTML 到达变成像素所花费的总时间,是性能优化的最终指标。
    T渲染T_{\text{渲染}}
  2. 五个分量解析(读菜单)、样式(定做法)、布局(排座位)、绘制(上装饰)、合成(端上桌)各占一段耗时,互不包含。
    T解析+T样式+T布局+T绘制+T合成T_{\text{解析}} + T_{\text{样式}} + T_{\text{布局}} + T_{\text{绘制}} + T_{\text{合成}}
  3. 等号的意义总时间是加法关系,这意味着只要某一环节被跳过(例如纯合成动画),整体效果立竿见影。
    T渲染=∑i=15TiT_{\text{渲染}} = \sum_{i=1}^{5} T_i

解析HTML建DOM

跑单员接到一张皱巴巴的手写菜单,第一件事是把它翻译清楚。浏览器的解析阶段,就是把网络上传来的一串字节流,变成自己能理解的结构化数据。如果菜单字迹潦草、纸张破损,翻译就会变慢——对应到网页,就是 HTML 体积过大或编码混乱导致首屏延迟。

解析分三步走:先把 0/1 字节按编码表解码成字符,再把字符按照 HTML 语法切分成一枚枚 Token(开始标签、结束标签、属性、文本),最后按标签的嵌套关系搭出一棵节点树。你可以把这三步想象成:先誊清字迹,再给菜单逐行圈出「菜名」「做法」「备注」,最后把这些条目按顺序挂到侯菜墙上。

这棵节点树就是 DOM(文档对象模型)。它是 JavaScript 操作的入口,也是后面所有阶段的原料。有一个重要细节:遇到 <script> 标签时,解析会被打断——因为脚本可能修改 DOM,浏览器必须停下来等它执行完。这也是为什么前端圈流传「脚本放底部、加 defer、给图片设宽高」的优化口诀。

这就是为什么「减少 DOM 数量、推迟 script 加载」能加快首屏——相当于给跑单员一份打印清晰的菜单,别让他在辨认字迹上浪费功夫。

DOM=建树(标记化(解码(字节流)))\text{DOM} = \text{建树}\Big(\text{标记化}\big(\text{解码}(\text{字节流})\big)\Big)
公式解读:DOM 是「建树」作用于「标记化」作用于「解码」的结果,从字节流到节点树层层加工,顺序不能乱。
  1. 解码二进制字节流按编码表(如 UTF-8)转换成可读字符,像是把潦草字迹誊写清楚。
    字节流→解码字符\text{字节流} \xrightarrow{\text{解码}} \text{字符}
  2. 标记化字符流按照 HTML 语法切分成开始标签、结束标签、文本等 Token,相当于给菜单逐行圈出条目。
    字符→标记化Token\text{字符} \xrightarrow{\text{标记化}} \text{Token}
  3. 建树Token 按嵌套关系生成父子节点,组成 DOM 树,成为后续所有阶段的输入原料。
    Token→建树DOM 树\text{Token} \xrightarrow{\text{建树}} \text{DOM 树}
  4. 复合函数顺序整个解析是从右向左逐步加工的,任何一步的延迟都会拖慢整条流水线。
    DOM=建树(标记化(解码(字节流)))\text{DOM} = \text{建树}(\text{标记化}(\text{解码}(\text{字节流})))

CSSOM样式计算

菜单读懂了,接下来配菜师打开主厨手册——CSS。但并非所有菜都按同一个做法处理:有的要清蒸,有的要爆炒。样式计算,就是给每个 DOM 节点匹配「这道菜该怎么做」。配菜师先快速浏览整本手册,把规则整理成一本索引——这就是 CSSOM(CSS 对象模型)。

浏览器先解析 CSS 生成 CSSOM,然后拿着每个 DOM 节点,从右向左匹配选择器。从右向左的意思是:先找出满足最右侧条件的元素,再逐级向上验证父级是否匹配。比如 .nav a 会先找所有 a,再确认它们的祖先有没有 .nav。这样做的好处是命中率高、过滤快,但选择器层数太多,回溯次数也会暴涨,像查一本没有目录的菜谱一样费劲。

当多个规则同时命中同一个元素时,主厨靠「层叠」规则定胜负。判断标准是特异性(specificity):行内样式权重最高,其次是 id 选择器,再次是 class/属性/伪类,最后是标签/伪元素。简化成加权打分,就是下面这条公式。

配菜师匹配规则时最怕的就是选择器绕弯。少写深层的后代选择器,就像给主厨一本目录清晰的菜谱——翻得快,菜就上得快。

S=I×1000+C×100+E×10+TS = I \times 1000 + C \times 100 + E \times 10 + T
公式解读:特异性得分 = 行内×1000 + id×100 + class×10 + 标签×1,得分高者胜出。不同浏览器实现的数值比例略有差异,但四档的权重层级关系一致。
  1. 行内样式 I写在 style 属性里的声明,权重最高,几乎不可被覆盖(除非对方用 !important)。
    I×1000I \times 1000
  2. id 选择器 C#nav 这类选择器,一个页面里唯一,权重次之。
    C×100C \times 100
  3. class/属性 E.btn、[type="text"]、:hover,数量多但权重低一档。
    E×10E \times 10
  4. 标签 Tdiv、span、::before,权重最低。
    T×1T \times 1
  5. 加权加总四个维度分别计数后求和,比较两个选择器的得分,谁高谁生效。
    S=I×1000+C×100+E×10+TS = I \times 1000 + C \times 100 + E \times 10 + T

布局与回流

菜的做法定了,接下来摆桌员出场。他要计算每张桌子的位置、大小、间距,确保顾客坐下不拥挤——这就是布局阶段。浏览器根据文档流、浮动、定位等规则,为每个元素算出精确的几何坐标。布局的理论基础,是盒模型。

每个 HTML 元素都是一个盒子,从内到外依次是内容区、内边距(padding)、边框(border)、外边距(margin)。布局阶段要算清楚的核心问题只有一个:这个盒子到底占了多大地方、放在哪里。用内容盒模型的算法来说,一个元素的水平占地宽度等于内容宽度加上左右内边距和左右边框。

一旦某个元素的几何属性发生改变——比如改了 width、height、margin、字体大小——摆桌员就可能要把整排餐桌重新排一遍。这个过程就叫回流(reflow,也叫重排)。回流是整个渲染管线里最贵的一环,而且它的代价会随 DOM 节点数量线性增长:节点越多,重排越慢。所以减少回流,永远是性能优化的第一优先级。

W盒子=W内容+2×(P内边距+B边框)W_{\text{盒子}} = W_{\text{内容}} + 2 \times (P_{\text{内边距}} + B_{\text{边框}})
公式解读:盒子实际占宽 = 内容宽 + 左右内边距 + 左右边框。布局阶段就是基于这些值计算坐标,而 offsetWidth 读取的正是这个结果。
  1. W_内容元素内容区的宽度,由 width 属性决定,文字和子元素都在这里排队。
    W内容W_{\text{内容}}
  2. P 内边距padding 会撑大盒子,给内容留出呼吸空间。
    2×P内边距2 \times P_{\text{内边距}}
  3. B 边框border 同样占位,是盒子的外壁。
    2×B边框2 \times B_{\text{边框}}
  4. 整体求和左右各一份,加起来就是盒子在页面上的实际占地宽度。
    W盒子=W内容+2P+2BW_{\text{盒子}} = W_{\text{内容}} + 2P + 2B

绘制与重绘

桌子摆好后,摆盘师上场。他要给每个元素填充背景色、描边框、写文字、加阴影——这就是绘制阶段。浏览器把布局算出的几何信息转换成一张张「绘制指令」清单,再由渲染进程逐条执行,把像素填进画布里。你可以把它想象成:桌位图已经画好,现在开始涂色。

如果页面变化只涉及外观,而不涉及几何——比如改了 color、background-color、box-shadow——布局不用重来,摆盘师只需要把对应区域重新涂一遍。这个操作叫重绘(repaint)。它比回流便宜,因为它不需要重新计算任何坐标,只是重新上色。

但重绘也不是免费的。它的开销与重绘面积成正比,面积越大越贵;如果某个区域每一帧都在重绘,主线程照样会被占满。更关键的是,回流发生之后必然跟着重绘——几何都变了,颜色不可能还保持原样。所以二者的关系是:回流 ≥ 重绘,重绘只是回流的下半场。

所以后厨的行话是「宁换桌布,不挪桌子」——能用重绘解决的问题,别让它升级成回流。改背景色远比改宽度便宜。

C重绘=k×A区域,C回流≫C重绘C_{\text{重绘}} = k \times A_{\text{区域}}, \qquad C_{\text{回流}} \gg C_{\text{重绘}}
公式解读:重绘开销正比于重绘面积乘系数 k;回流则比单纯重绘贵得多,因为几何重算之后必然还要重新绘制。
  1. k 系数每像素的绘制成本,取决于指令复杂度——阴影、圆角、渐变都会把 k 拉大。
    kk
  2. A 区域被重绘的面积,越大越贵。
    A区域A_{\text{区域}}
  3. C_回流 ≫ C_重绘回流 = 几何重算 + 重绘,成本远高于单独重绘。
    C回流≫C重绘C_{\text{回流}} \gg C_{\text{重绘}}

合成与分层

餐厅里常有驻唱歌手,聪明的老板把他安排在一个独立的小舞台上。换歌时只需调整舞台灯光,完全不用搬动任何餐桌——这就是合成层的思路。浏览器把页面拆成多个图层,每个图层先独立画好,最后像叠卡片一样合成在一起。

平时修改布局属性,图层内部要重新布局、重新绘制;但如果动画只作用于 transform(位移、旋转、缩放)或 opacity(透明度),情况就完全不同了。浏览器可以直接把整个图层搬到 GPU 上,让合成器线程负责移动和叠加,主线程可以完全休息。这就是为什么「能用 transform 就不用 left」会成为前端铁律。

合成是五个阶段里最便宜的环节,因为它不动几何、不重新绘制,只做图层间的叠放。不过图层是稀缺资源——每多一层就多一块 GPU 内存。will-change: transform 可以提前告诉浏览器「这个元素要动画,先把它独立成层」,但如果滥用,图层数量爆炸,合成阶段每一帧都要遍历更多层,反而更卡。

记住:独立舞台是给「常驻歌手」准备的,不是给每个路人甲都搭一个。图层贵,慎用 will-change。

T合成动画≈T合成≪T回流+T重绘T_{\text{合成动画}} \approx T_{\text{合成}} \ll T_{\text{回流}} + T_{\text{重绘}}
公式解读:用 transform/opacity 做动画的耗时,几乎只等于合成阶段的耗时,远远小于回流加重绘的开销。
  1. T_合成动画transform 做位移动画的单帧总开销。
    T合成动画T_{\text{合成动画}}
  2. ≈ T_合成只触发合成阶段,跳过布局与绘制。
    T合成动画≈T合成T_{\text{合成动画}} \approx T_{\text{合成}}
  3. ≪ 右侧回流 + 重绘的成本动辄是合成的数倍到数十倍,差距随 DOM 规模放大。
    T合成≪T回流+T重绘T_{\text{合成}} \ll T_{\text{回流}} + T_{\text{重绘}}

性能优化策略

后厨效率最高的点单方式是什么?一次点齐,后厨一次做完。前端也是一样:把多次 DOM 修改合并成一次提交,让摆桌员只返工一轮。常见的姿势有:用切换 class 代替逐条改 style、用 DocumentFragment 离线拼接节点、把动画元素设为 position: fixed 或 absolute 脱离文档流,缩小回流波及范围。

还有一个容易被忽略却极其重要的原则——读写分离。读取 offsetHeight、getBoundingClientRect() 这类布局属性时,浏览器如果发现布局是「脏」的,会强制先同步补一次布局再返回值。如果你写一下、读一下、再写一下、再读一下,每次写都会触发回流,形成「布局抖动」(layout thrashing)。

举个具体场景:循环里给 100 个元素改宽度,每次改完立刻读 offsetTop 来校验。表面上只改了 100 次,实际触发了 100 次回流。正确的做法是:先把所有 offsetTop 读出来存进数组,再统一改宽度——读是一波,写是一波,回流只发生一次。

性能优化策略说到底就一句话:别让后厨反复返工。把改动攒成一波提交、把动画放到独立图层、把读写顺序理顺,页面自然顺滑如丝。

N批处理=1,N交错=mN_{\text{批处理}} = 1, \qquad N_{\text{交错}} = m
公式解读:把 m 次写操作合并后统一提交,回流次数从 m 次降为 1 次;交错读写会让回流次数随操作次数线性增长。
  1. N_批处理 = 1合并写操作后,布局只重算一次。
    N批处理=1N_{\text{批处理}} = 1
  2. N_交错 = m读-写-读-写交替,每次写后读取都强制同步布局,回流 m 次。
    N交错=mN_{\text{交错}} = m
  3. 比较m 越大,批处理收益越明显。
    N批处理≪N交错N_{\text{批处理}} \ll N_{\text{交错}}

AI 应用场景

超长列表虚拟滚动优化

阶段:前端渲染性能优化

解决的问题:长列表一次性渲染上万条 DOM,频繁回流导致白屏卡顿。传统方案一次性渲染全部节点,布局阶段要重排整棵树。AI 辅助分析用户视口与滚动位置,动态计算可见区域,只渲染可视区条目并预留占位高度,把回流范围限制在极小区域。

具体案例:某数据大屏展示 10 万条日志,全量渲染首屏耗时 8 秒;改为虚拟滚动 + transform 位移方案后,首屏小于 0.5 秒,滚动动画稳定在 60fps。

页面动画性能监控告警

阶段:可观测性与性能监控

解决的问题:线上卡顿难以定位,人工复现成本高。通过 Performance Observer 采集长任务与 Layout 时长打点,再结合异常检测算法,自动识别哪些操作触发了大量回流或重绘,并给出优化建议。

具体案例:电商大促页面接入 LCP/CLS 监控后,系统自动识别出「图片懒加载导致布局偏移」的回流问题,工程师定位并修复后,CLS 指标下降 60%。

H5 动效选型建议

阶段:交互体验设计

解决的问题:开发人员不确定动画该用 left 还是 transform,两个方案代码相似但性能天差地别。借助合成层分析工具,自动检测动画属性是否触发回流/重绘,并给出「改用 transform 或 opacity」的修改建议。

具体案例:某移动端小游戏用 transform 重写卡片翻转动画后,掉帧率从 35% 降为 2%,主线程长任务基本消失。

Python 演示

python78 行
# -*- coding: utf-8 -*-"""模拟浏览器渲染管线中的回流成本对比. 用一棵简化 DOM 树演示两种更新策略:A 读写交错, 每次写后立即读取, 反复触发回流;B 先批量写完再统一读取, 只回流一次.""" import time  class Node:    """简化 DOM 节点."""     def __init__(self, name, children=None):        self.name = name        self.children = children or []        self.width = 100  def build_dom(depth, branch):    """构建深度为 depth, 分支数为 branch 的 DOM 树."""    if depth == 0:        return Node("leaf")    node = Node("div")    for _ in range(branch):        node.children.append(build_dom(depth - 1, branch))    return node  def reflow_cost(root):    """统计一次回流需要重算的节点数量."""    count = 1    for child in root.children:        count += reflow_cost(child)    return count  def strategy_interleaved(root, loops, gap_seconds):    """策略A: 写一次读一次, 读时强制同步布局."""    times = 0    nodes = 0    for _ in range(loops):        root.width += 1        times += 1        nodes += reflow_cost(root)        time.sleep(gap_seconds)    return times, nodes  def strategy_batched(root, loops):    """策略B: 写入全部完成后再统一触发一次回流."""    for _ in range(loops):        root.width += 1    times = 1    nodes = reflow_cost(root)    return times, nodes  def main():    dom = build_dom(depth=4, branch=3)    loops = 30     start = time.perf_counter()    a_times, a_nodes = strategy_interleaved(dom, loops, 0.0002)    a_time = time.perf_counter() - start     start = time.perf_counter()    b_times, b_nodes = strategy_batched(dom, loops)    b_time = time.perf_counter() - start     print("策略A 读写交错: 回流 %d 次, 重算节点 %d 个, 耗时 %.4f 秒" % (a_times, a_nodes, a_time))    print("策略B 批量读写: 回流 %d 次, 重算节点 %d 个, 耗时 %.4f 秒" % (b_times, b_nodes, b_time))    print("结论: 批处理把回流次数从 %d 次降到 1 次" % loops)  if __name__ == "__main__":    main()
目的:用一棵 121 个节点的简化 DOM 树,对比「读写交错」和「批量读写」两种策略触发的回流次数与耗时,直观验证读写分离原则。
  1. def build_dom(depth, branch):递归生成简化 DOM 树,深度 4、分支 3,共 121 个节点。
  2. def reflow_cost(root):模拟一次回流,深度优先统计需要重算的节点数量。
  3. def strategy_interleaved(root, loops, gap_seconds):策略A:每写一次就读取一次布局,读时强制触发回流。
  4. nodes += reflow_cost(root)这一步对应浏览器读取 offsetHeight 时的「强制同步布局」。
  5. def strategy_batched(root, loops):策略B:先把所有写操作做完,期间不读布局。
  6. times = 1批量模式下统一读取,回流只发生一次。
  7. dom = build_dom(depth=4, branch=3)生成测试用 DOM 树,节点越多,回流代价差异越明显。
  8. print("策略A 读写交错: 回流 %d 次, 重算节点 %d 个, 耗时 %.4f 秒" % (a_times, a_nodes, a_time))输出策略A的统计结果。

常见误区

误区我觉得「重绘」就是重新画一遍,而「回流」只是数据流动一下,应该重绘更耗性能,所以优化时应该优先减少重绘。

正解恰恰相反。回流(布局)要重算几何位置,而重绘只是重新上色。回流必然伴随重绘,重绘不一定伴随回流。比喻:挪动整排餐桌(回流)必然要重新铺桌布(重绘);但只换桌布(重绘)不需要挪桌子。所以回流代价远大于重绘,优化时应优先减少回流。

为何常见:「重绘」字面上有「重新绘制」的完整动作感,而「回流」听起来抽象,初学者容易被字面意思误导,忽略了布局重算的几何开销。

误区我已经把 100 次修改合并成 1 次了,为什么页面还是卡?是不是操作次数还不够少?

正解次数少不等于顺序好。如果在修改之间穿插读取 offsetHeight 等布局属性,浏览器会被迫「写→读→写→读」地反复同步布局,这就是布局抖动。正确做法是读写分离:先批量读取并缓存,再统一写入,把 m 次回流合并为 1 次。

为何常见:只关注操作「数量」而忽略操作「顺序」,因为读写抖动是隐性的,不借助性能分析工具很难观察到。

误区既然 transform 只触发合成,那我给所有元素都加上 transform 和 will-change,页面动画一定飞起。

正解每个合成层都要占用 GPU 内存。层太多时,合成阶段每帧要遍历更多图层,内存压力和 GPU 开销反而拖慢帧率。transform 只是把成本从主线程转移到合成线程,不是凭空消失。应该只对真正需要动画的元素使用,动画结束后移除 will-change。

为何常见:教程常说「用 transform 替代 left」,初学者把「更优」误解为「免费」,忽视了分层同样有成本。

误区will-change 就是给浏览器的加速 buff,我给所有要动的元素都加上 `will-change: transform`,结果手机发热更严重了。

正解will-change 的作用是提前告诉浏览器「这个元素要动画」,浏览器会提前把它变成独立合成层。层数过多会耗尽 GPU 内存,合成开销反而上升。建议只对即将动画、且动画持续的少量元素使用,动画结束后及时移除。

为何常见:把「提示」当成「强制加速」,不理解合成层是稀缺资源,用「多多益善」的思路套用到性能优化上。

面试题

Q1. 浏览器渲染管线的正确执行顺序是?

单选题 · easy

  • A.解析→样式计算→布局→绘制→合成← 正确答案
  • B.布局→解析→绘制→样式计算→合成
  • C.合成→绘制→布局→样式计算→解析
  • D.解析→布局→样式计算→合成→绘制

答案:A。渲染管线固定顺序为解析→样式计算→布局→绘制→合成,如同后厨先读菜单、再定做法、然后摆桌、最后上菜。B 把布局提到最前,C 完全倒序,D 把合成提前,均不符合。

Q2. 下列哪个操作会触发回流(重排)?

单选题 · easy

  • A.修改元素的 color 颜色值
  • B.修改元素的 width 宽度值← 正确答案
  • C.修改元素的 opacity 透明度
  • D.修改元素的 transform 位移

答案:B。width 是几何属性,改动后摆桌员必须重排整片区域,触发回流。color 只影响绘制,opacity 与 transform 只触发合成,都不会引发布局重算。

Q3. 读取下面哪个属性会强制浏览器同步执行布局?

单选题 · mid

  • A.element.firstChild 子节点
  • B.element.offsetHeight 高度← 正确答案
  • C.element.nodeName 节点名
  • D.element.textContent 文本

答案:B。offsetHeight 属于布局信息。读取时浏览器发现布局是脏的,会强制先同步执行一次布局再返回值,这正是「强制同步布局」的来源。其余三个属性与几何无关,不会触发。

Q4. 下列优化手段中,最能避免动画引发大面积回流的是?

单选题 · mid

  • A.用 left 做水平位移动画
  • B.用 transform 做位移动画← 正确答案
  • C.用 margin-left 做水平位移
  • D.用 top 做垂直位移动画

答案:B。transform 让元素进入独立合成层,动画只做图层平移,不重排不重绘。left、margin、top 都会改变几何属性,触发回流,相当于把整排餐桌反复挪动。

Q5. 关于 CSS 特异性(优先级)计算,权重最高的是?

单选题 · mid

  • A.行内样式 style 属性← 正确答案
  • B.id 选择器 #nav 唯一
  • C.class 选择器 .btn 复用
  • D.标签选择器 div 基础

答案:A。特异性公式 S = I×1000 + C×100 + E×10 + T 中,行内样式 I 权重最高,其次 id、class、标签。行内样式相当于主厨亲手批注,优先级自然最高。

Q6. 关于回流与重绘的关系,下列说法正确的是?

单选题 · hard

  • A.重绘一定会触发回流
  • B.回流必然伴随重绘← 正确答案
  • C.回流与重绘互不相关
  • D.重绘的开销一定大于回流

答案:B。回流是几何重算,几何变了区域里的像素必然要重新画,所以回流必然伴随重绘;反过来只改颜色则只重绘不回流。A 说反了,C 错误,D 也错误——回流比重绘贵得多。

Q7. 「强制同步布局」导致性能问题的正确应对方法是?

单选题 · mid

  • A.写一次立刻读一次保证数据最新
  • B.先统一读取布局信息再批量写入← 正确答案
  • C.每次写入后即时读取做校验
  • D.用 setTimeout 延迟所有读取操作

答案:B。读写交错会形成布局抖动,每次写后读都强制回流。先统一读取并缓存布局信息,再批量写入,回流次数从 m 次降到 1 次。延迟读取并不能消除抖动,只会把问题推迟。

Q8. 下列哪些操作只会触发重绘,而不会触发回流?

多选题 · easy

  • A.修改 background-color 背景色← 正确答案
  • B.修改 color 文字颜色← 正确答案
  • C.修改 width 元素宽度
  • D.修改 font-size 字体大小

答案:AB。background-color 与 color 只影响绘制阶段,属于重绘。width 与 font-size 改变几何布局,会触发回流。比喻:换桌布是重绘,挪桌子是回流。

Q9. 以下哪些手段可以有效减少回流带来的性能损耗?

多选题 · hard

  • A.用 DocumentFragment 批量插入节点← 正确答案
  • B.切换 class 而不是逐条修改 style← 正确答案
  • C.动画元素用 fixed 脱离文档流← 正确答案
  • D.每次改完立即读取 offsetHeight 校验

答案:ABC。DocumentFragment 离线批量插入、切换 class 一次性应用样式、fixed 脱离文档流缩小回流范围,都是有效手段。D 每次改后立即读取会引入强制同步布局,增加回流次数,反而更糟。

Q10. 关于浏览器渲染性能优化,下列说法正确的是?

多选题 · extreme

  • A.读取 offsetHeight 不会触发强制同步布局
  • B.读写分离可以减少布局抖动次数← 正确答案
  • C.transform 做位移动画会触发回流重绘
  • D.回流发生之后通常伴随一次重绘← 正确答案

答案:BD。读取 offsetHeight 会强制同步布局,A 错;读写分离正是为了减少布局抖动,B 对;transform 只走合成阶段,不触发回流重绘,C 错;回流后几何变化必然导致重绘,D 对。