[{"data":1,"prerenderedAt":651},["ShallowReactive",2],{"page-\u002Fbrowser\u002Fwebkit与重绘重排":3},{"id":4,"title":5,"body":6,"description":12,"extension":641,"meta":642,"navigation":438,"path":647,"seo":648,"stem":649,"__hash__":650},"content\u002Fbrowser\u002Fwebkit与重绘重排.md","Webkit与重绘重排",{"type":7,"value":8,"toc":637},"minimark",[9,13,17,25,57,63,69,108,111,130,133,136,139,142,164,167,170,263,266,311,345,348,351,543,546,549,598,611,615,634],[10,11,12],"p",{},"重排和重绘是浏览器渲染引擎的工作机制，而 WebKit 正是最著名、历史最悠久的渲染引擎之一，",[14,15,16],"h2",{"id":16},"webkit里真实名称",[10,18,19,20,24],{},"虽然我们常把“重排”叫 Reflow，但在 WebKit 的源码和核心架构里，它更标准的叫法是 ",[21,22,23],"strong",{},"Layout（布局）","。",[26,27,28,48],"ul",{},[29,30,31,34,35,39,40,43,44,47],"li",{},[21,32,33],{},"重排 (Reflow \u002F Layout)","：在 WebKit 中，对应的是 ",[36,37,38],"code",{},"RenderObject","（渲染对象）树去计算每一个盒子几何位置的过程。WebKit 有一个专门的内核类叫 ",[36,41,42],{},"RenderView"," 和 ",[36,45,46],{},"RenderBlock"," 来负责执行 Layout 算法。",[29,49,50,34,53,56],{},[21,51,52],{},"重绘 (Repaint)",[36,54,55],{},"GraphicsContext"," 去重新填充像素的过程。",[10,58,59],{},[60,61,62],"em",{},"(注：Reflow 这个词最早是 Firefox 的 Gecko 引擎提出的，后来因为太形象，大家就把它当成了重排的通用代名词。)",[10,64,65,66,24],{},"在 WebKit 的架构里，一次页面的渲染大致是：",[36,67,68],{},"DOM 树 -> 样式计算 -> Render 树 -> Layout (重排) -> Paint (重绘) -> Composite (合成)",[26,70,71,84,94],{},[29,72,73,74,77,78,80,81,24],{},"当 JS 修改了元素的 ",[36,75,76],{},"width"," 时，WebKit 内部的 ",[36,79,38],{}," 会标记自己为 ",[36,82,83],{},"needsLayout",[29,85,86,87,90,91,24],{},"在下一帧到来时，WebKit 会调用 ",[36,88,89],{},"layout()"," 方法，重新计算整棵树，这就是",[21,92,93],{},"重排",[29,95,96,97,100,101,104,105,24],{},"重排完成后，WebKit 发现外观变了，会标记 ",[36,98,99],{},"needsPaint","，然后调用 ",[36,102,103],{},"paint()"," 方法，把像素画到内存的位图里，这就是",[21,106,107],{},"重绘",[10,109,110],{},"重排和重绘是很消耗性能的，所以webkit以及后来的blink引擎，选择引入了渲染层和图形层概念：",[26,112,113,124],{},[29,114,115,116,119,120,123],{},"如果你只修改了 ",[36,117,118],{},"transform"," 或 ",[36,121,122],{},"opacity","，WebKit 根本不会触发 Layout 和 Paint，而是直接让 GPU 重新组合已有的图层（Composite）。",[29,125,126,127,24],{},"这就是为什么现代 WebKit 引擎能做到 60fps 丝滑动画的底层秘密——",[21,128,129],{},"绕开了重排和重绘",[10,131,132],{},"渲染层和图形层：",[10,134,135],{},"Renderlayer是浏览器在dom树和渲染树之后建立的逻辑分层结构，负责解决元素在视觉树的堆叠和覆盖问题，让决定元素在z轴上的堆叠顺序，比如z-index，确保透明、重叠元素能够正确显示，并处理页面内容的渲染层级。",[10,137,138],{},"文档根节点和就具有明确的css定位比如relative、absolute、transform 、透明、overflow、mask、filter或者包含3d\u002F加速2d canvas及video元素的节点才会创建。renderlayerf不参与最终的gpu渲染，它的树结构是为了下一步的合成做逻辑准备的。",[10,140,141],{},"grapicslayer才是renderlayer的物理存储实现，也是真正作为纹理上传给gpu进行合成的层，负责管理实际的后端存储空间，将renderlayer的内容绘制成位图，最终交给合成器compositor进行gpu合成，输出最终的屏幕图像。一个 RenderLayer 可能对应一个或多个 GraphicsLayer（用于管理前景、背景等不同存储），而 GraphicsLayer 则构成了与 RenderLayer 树并行的另一棵树。",[10,143,144,145,148,149,152,153,152,156,159,160,163],{},"它们协同的核心，就是浏览器为了极致性能而构建的",[21,146,147],{},"硬件加速合成化渲染","流程。可以简化为：",[36,150,151],{},"DOM Tree"," -> ",[36,154,155],{},"RenderObject Tree",[36,157,158],{},"RenderLayer Tree","（逻辑分层）-> ",[36,161,162],{},"GraphicsLayer Tree","（物理存储）-> GPU 合成",[10,165,166],{},"对于实践来说，我们得迎合渲染引擎，实现极致的高性能渲染，核心是想尽一切办法让CSS的变化停留在合成composite阶段，彻底跳过layout和paint环节。如果说你就用with、height、padding、margin、border、top、left、bottom、right、font-size做动画的话，浏览器的每一帧都在经历重排和重绘。",[10,168,169],{},"这种情况下我们会把动画交给gpu处理，webkit引擎在处理transform和opacity的时候会把他们提升为一个独立的合成层，叫做compositing layer：",[171,172,177],"pre",{"className":173,"code":174,"language":175,"meta":176,"style":176},"language-css shiki shiki-themes github-light github-dark","\u002F* 极致性能：不重排、不重绘，仅在 GPU 合成阶段处理 *\u002F\n.box {\n  transform: translateX(100px) scale(1.1);\n  transition: transform 0.3s ease;\n}\n","css","",[36,178,179,188,199,236,257],{"__ignoreMap":176},[180,181,184],"span",{"class":182,"line":183},"line",1,[180,185,187],{"class":186},"sJ8bj","\u002F* 极致性能：不重排、不重绘，仅在 GPU 合成阶段处理 *\u002F\n",[180,189,191,195],{"class":182,"line":190},2,[180,192,194],{"class":193},"sScJk",".box",[180,196,198],{"class":197},"sVt8B"," {\n",[180,200,202,206,209,212,215,218,222,225,228,230,233],{"class":182,"line":201},3,[180,203,205],{"class":204},"sj4cs","  transform",[180,207,208],{"class":197},": ",[180,210,211],{"class":204},"translateX",[180,213,214],{"class":197},"(",[180,216,217],{"class":204},"100",[180,219,221],{"class":220},"szBVR","px",[180,223,224],{"class":197},") ",[180,226,227],{"class":204},"scale",[180,229,214],{"class":197},[180,231,232],{"class":204},"1.1",[180,234,235],{"class":197},");\n",[180,237,239,242,245,248,251,254],{"class":182,"line":238},4,[180,240,241],{"class":204},"  transition",[180,243,244],{"class":197},": transform ",[180,246,247],{"class":204},"0.3",[180,249,250],{"class":220},"s",[180,252,253],{"class":204}," ease",[180,255,256],{"class":197},";\n",[180,258,260],{"class":182,"line":259},5,[180,261,262],{"class":197},"}\n",[10,264,265],{},"如果说一个复杂组件，比如说长列表的其中一项，或者一个复杂的图表，它的内部发生了重排，但是你不喜欢它影响到整个页面，你可以使用contain属性，强制浏览器把该元素以及其子树的布局、绘制与外部隔离：",[171,267,269],{"className":173,"code":268,"language":175,"meta":176,"style":176},".card {\n  \u002F* 告诉引擎：我内部变化不要影响外面，我也尽量不受外面影响 *\u002F\n  contain: layout paint style;\n  \u002F* 或者直接用最强隔离 *\u002F\n  \u002F* contain: strict; *\u002F\n}\n",[36,270,271,278,283,296,301,306],{"__ignoreMap":176},[180,272,273,276],{"class":182,"line":183},[180,274,275],{"class":193},".card",[180,277,198],{"class":197},[180,279,280],{"class":182,"line":190},[180,281,282],{"class":186},"  \u002F* 告诉引擎：我内部变化不要影响外面，我也尽量不受外面影响 *\u002F\n",[180,284,285,288,291,294],{"class":182,"line":201},[180,286,287],{"class":204},"  contain",[180,289,290],{"class":197},": layout paint ",[180,292,293],{"class":204},"style",[180,295,256],{"class":197},[180,297,298],{"class":182,"line":238},[180,299,300],{"class":186},"  \u002F* 或者直接用最强隔离 *\u002F\n",[180,302,303],{"class":182,"line":259},[180,304,305],{"class":186},"  \u002F* contain: strict; *\u002F\n",[180,307,309],{"class":182,"line":308},6,[180,310,262],{"class":197},[26,312,313,321,329],{},[29,314,315,320],{},[21,316,317],{},[36,318,319],{},"contain: layout","：内部布局变化，绝对不会引发外部元素重排。",[29,322,323,328],{},[21,324,325],{},[36,326,327],{},"contain: paint","：内部元素超出边界绝对不会画到外面（类似 overflow: hidden 但不创建 BFC）。",[29,330,331,336,337,340,341,344],{},[21,332,333],{},[36,334,335],{},"content-visibility: auto","：这是配合 ",[36,338,339],{},"contain"," 的大杀器。对于屏幕外（未滚动到可视区域）的 DOM，浏览器直接",[21,342,343],{},"跳过其渲染树构建、布局和绘制","，性能提升极其恐怖。",[10,346,347],{},"如果是颜色，背景等必须重绘的内容，我们会尽量减少重绘的面积，用box-shadow来代替外部图片或者大体积元素，避免大面积使用极高模糊度的filter: blur，这个极度消耗gpu性能，尤其是移动端。如果某个元素需要频繁改变样式的话，并且他在屏幕中很小，可以把他的z-index设置为负数或者将其相对定位，避免触发大范围的脏矩形重绘。",[10,349,350],{},"由于webkit的布局计算时批量异步处理的，在下一帧执行，因此js操作css容易踩坑，如果你在同一个循环里先修改了样式又立即读取了样式，浏览器为了给你正确的结果，会立即打断异步队列，强制执行重排：",[171,352,356],{"className":353,"code":354,"language":355,"meta":176,"style":176},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F 极其糟糕的代码：循环内读写交替，导致强制连续重排\nfor (let i = 0; i \u003C 100; i++) {\n  el.style.left = el.offsetLeft + 1 + 'px'; \u002F\u002F 读(引发重排) -> 写 -> 读(引发重排)...\n}\n\n\u002F\u002F 读写分离：先集中读，再集中写\nconst lefts = [];\nfor (let i = 0; i \u003C 100; i++) lefts.push(el[i].offsetLeft); \u002F\u002F 统一读\nfor (let i = 0; i \u003C 100; i++) el[i].style.left = lefts[i] + 1 + 'px'; \u002F\u002F 统一写\n","js",[36,357,358,363,401,430,434,440,445,460,497],{"__ignoreMap":176},[180,359,360],{"class":182,"line":183},[180,361,362],{"class":186},"\u002F\u002F 极其糟糕的代码：循环内读写交替，导致强制连续重排\n",[180,364,365,368,371,374,377,380,383,386,389,392,395,398],{"class":182,"line":190},[180,366,367],{"class":220},"for",[180,369,370],{"class":197}," (",[180,372,373],{"class":220},"let",[180,375,376],{"class":197}," i ",[180,378,379],{"class":220},"=",[180,381,382],{"class":204}," 0",[180,384,385],{"class":197},"; i ",[180,387,388],{"class":220},"\u003C",[180,390,391],{"class":204}," 100",[180,393,394],{"class":197},"; i",[180,396,397],{"class":220},"++",[180,399,400],{"class":197},") {\n",[180,402,403,406,408,411,414,417,420,424,427],{"class":182,"line":201},[180,404,405],{"class":197},"  el.style.left ",[180,407,379],{"class":220},[180,409,410],{"class":197}," el.offsetLeft ",[180,412,413],{"class":220},"+",[180,415,416],{"class":204}," 1",[180,418,419],{"class":220}," +",[180,421,423],{"class":422},"sZZnC"," 'px'",[180,425,426],{"class":197},"; ",[180,428,429],{"class":186},"\u002F\u002F 读(引发重排) -> 写 -> 读(引发重排)...\n",[180,431,432],{"class":182,"line":238},[180,433,262],{"class":197},[180,435,436],{"class":182,"line":259},[180,437,439],{"emptyLinePlaceholder":438},true,"\n",[180,441,442],{"class":182,"line":308},[180,443,444],{"class":186},"\u002F\u002F 读写分离：先集中读，再集中写\n",[180,446,448,451,454,457],{"class":182,"line":447},7,[180,449,450],{"class":220},"const",[180,452,453],{"class":204}," lefts",[180,455,456],{"class":220}," =",[180,458,459],{"class":197}," [];\n",[180,461,463,465,467,469,471,473,475,477,479,481,483,485,488,491,494],{"class":182,"line":462},8,[180,464,367],{"class":220},[180,466,370],{"class":197},[180,468,373],{"class":220},[180,470,376],{"class":197},[180,472,379],{"class":220},[180,474,382],{"class":204},[180,476,385],{"class":197},[180,478,388],{"class":220},[180,480,391],{"class":204},[180,482,394],{"class":197},[180,484,397],{"class":220},[180,486,487],{"class":197},") lefts.",[180,489,490],{"class":193},"push",[180,492,493],{"class":197},"(el[i].offsetLeft); ",[180,495,496],{"class":186},"\u002F\u002F 统一读\n",[180,498,500,502,504,506,508,510,512,514,516,518,520,522,525,527,530,532,534,536,538,540],{"class":182,"line":499},9,[180,501,367],{"class":220},[180,503,370],{"class":197},[180,505,373],{"class":220},[180,507,376],{"class":197},[180,509,379],{"class":220},[180,511,382],{"class":204},[180,513,385],{"class":197},[180,515,388],{"class":220},[180,517,391],{"class":204},[180,519,394],{"class":197},[180,521,397],{"class":220},[180,523,524],{"class":197},") el[i].style.left ",[180,526,379],{"class":220},[180,528,529],{"class":197}," lefts[i] ",[180,531,413],{"class":220},[180,533,416],{"class":204},[180,535,419],{"class":220},[180,537,423],{"class":422},[180,539,426],{"class":197},[180,541,542],{"class":186},"\u002F\u002F 统一写\n",[10,544,545],{},"优化技巧：",[10,547,548],{},"如果说发现某个动画掉帧，你可以认为创建一个单独的渲染层，RenderLayer->GraphicsLayer：",[171,550,552],{"className":173,"code":551,"language":175,"meta":176,"style":176},".fast-animation {\n  will-change: transform; \u002F* 提前告诉引擎：我马上要变了，把层建好 *\u002F\n  \u002F* 或者用 3D 变换欺骗引擎 *\u002F\n  transform: translateZ(0); \n}\n",[36,553,554,561,572,577,594],{"__ignoreMap":176},[180,555,556,559],{"class":182,"line":183},[180,557,558],{"class":193},".fast-animation",[180,560,198],{"class":197},[180,562,563,566,569],{"class":182,"line":190},[180,564,565],{"class":204},"  will-change",[180,567,568],{"class":197},": transform; ",[180,570,571],{"class":186},"\u002F* 提前告诉引擎：我马上要变了，把层建好 *\u002F\n",[180,573,574],{"class":182,"line":201},[180,575,576],{"class":186},"  \u002F* 或者用 3D 变换欺骗引擎 *\u002F\n",[180,578,579,581,583,586,588,591],{"class":182,"line":238},[180,580,205],{"class":204},[180,582,208],{"class":197},[180,584,585],{"class":204},"translateZ",[180,587,214],{"class":197},[180,589,590],{"class":204},"0",[180,592,593],{"class":197},"); \n",[180,595,596],{"class":182,"line":259},[180,597,262],{"class":197},[599,600,601],"blockquote",{},[10,602,603,604,607,608,24],{},"这个属性",[21,605,606],{},"绝不能滥用","。合成层极其消耗内存，层建得太多，会导致内存爆炸和页面卡顿。",[21,609,610],{},"只对即将发生动画、或者频繁变化的元素使用",[14,612,614],{"id":613},"webkit-和-blink-的关系","WebKit 和 Blink 的关系",[10,616,617,618,621,622,625,626,629,630,633],{},"Google Chrome 早期用的就是 WebKit 引擎，后来 Google 觉得 WebKit 的代码太老了，于是从 WebKit 中 Fork 出了一个分支，命名为 ",[21,619,620],{},"Blink","（现在 Edge、Opera 等都在用）。",[623,624],"br",{},"\n所以，你在 Chrome 开发者工具（Performance 面板）里看到的绿色 ",[36,627,628],{},"Layout"," 和紫色 ",[36,631,632],{},"Paint"," 块，其底层祖传代码和架构理念，正是来源于 WebKit",[293,635,636],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":176,"searchDepth":190,"depth":190,"links":638},[639,640],{"id":16,"depth":190,"text":16},{"id":613,"depth":190,"text":614},"md",{"date":643,"tags":644},"2026-10-01",[645,646,175],"browser","webkit","\u002Fbrowser\u002Fwebkit与重绘重排",{"description":12},"browser\u002Fwebkit与重绘重排","KbZMIiyEstniMtkMNSeW62O8USc1igmNQnbeprGi00Y",1792926853090]