Skip to content

结论先行(TL;DR)

会的。 浏览器只有在“渲染机会(rendering opportunity)”点才可能提交像素;在此之前:

  • 可以连续执行多个宏任务(Tasks)
  • 在这些任务里可以多次触发布局/回流计算(包括你代码里强制布局的同步读取);
  • 但仍然不发生绘制提交(像素不更新)。 原因:JS 执行期间不会绘制,浏览器有权跳过这一帧的绘制(无可见变化/后台/节流/时间片不够/微任务饥饿等)。

一帧内的“可能”时间线(简化版)

Task A(你的脚本/事件/定时器回调)
  ↳ 多次写样式/读布局(可能触发样式/布局计算)
Microtasks(Promise/queueMicrotask 等)

Task B(例如另一个 setTimeout/postMessage 回调)
  ↳ 再次写样式/读布局(再次触发布局计算)
Microtasks(再次)

requestAnimationFrame(若文档可见)
Microtasks(有的实现会在 rAF 后再跑一次)

渲染机会(样式→布局→绘制→合成)← 这一步**可能**被跳过
requestIdleCallback(若本帧还有空闲)

关键点:渲染机会在一串任务与微任务之后才出现;浏览器可根据负载与可见性跳过绘制。


为什么能出现“多次计算、不渲染”

  1. JS 主线程正在跑任务 浏览器不会中断 JS 去绘制。你在 Task 里频繁 offsetHeight/getComputedStyle 等,会强制样式/布局计算,但这些只是计算步骤不是像素提交

  2. 多个任务连着跑 比如密集的 setTimeout(0)postMessage/MessageChannel、事件风暴(滚轮、网络、I/O)。在下一次“渲染机会”到来前,可能已经跑了多个 Task

  3. 浏览器可以跳过绘制 无可见变化、标签页后台、节流/省电、超时长任务导致帧预算不足、或连续微任务造成的微任务饥饿,都会让这一帧的绘制被跳过

  4. 合成优化不等于每次绘制 某些变更(如 transform/opacity)可走合成器线程;主线程这侧即使做了样式/布局计算,也不一定每帧都提交像素


典型场景举例

  • 布局抖动(Layout Thrashing) 在一个或多个 Task 中交替“写样式 → 读布局 → 再写 → 再读”,会触发多次样式/布局计算;直到该回合结束且到了渲染机会,才可能绘制。

  • 密集计时器/消息队列 多个 setTimeout(0)postMessage 连续执行,挤占了帧预算,渲染点被推迟,之前的计算都只是“准备工作”。

  • 后台/节流 标签页隐藏时,浏览器把绘制频率降到极低;任务仍可跑(被节流后频率也会降),但几乎不提交像素。


和 Vue / nextTick 的关系

  • Vue 3 把渲染队列刷新(patch)安排在微任务中;nextTick 的回调在patch 之后、仍在微任务阶段。
  • 这意味着DOM 已更新,但像素提交仍要等到后续的渲染机会;若浏览器跳过这一帧绘制,你仍看不到视觉变化,直到下一次真正绘制发生。

如何识别你遇到了这种情况

  • requestAnimationFrame 时间戳:两次 rAF 之间的时间远超目标帧间隔(如 >16ms),说明帧被跳过/推迟
  • 长任务(Long Task)标记:Performance 面板看到 >50ms 的长任务,易导致绘制延后。
  • 布局与样式计算激增:Performance → Summary/Bottom-Up 里看到 Style/Layout 次数高但 Frames 提交少。

实战建议(避免“算了很多却没绘制”)

  1. 读写分离 在同一 Task 内,先批量读取布局,再批量写入样式,避免交替触发强制回流。

  2. requestAnimationFrame 对齐渲染节奏

    • 渲染相关写操作(尤其动画步进)放进 rAF;
    • 读操作可在 rAF 开头统一读取,写操作在 rAF 末尾统一写入。
  3. 控制任务密度

    • 不要滥用 setTimeout(0)/postMessage 连环调用;
    • 长计算用切片setTimeout/scheduler.postTask/requestIdleCallback)或搬到 Web Worker
  4. 避免微任务饥饿 连续 queueMicrotask/Promise.then 循环会推迟渲染;必要时插入一个宏任务切片。

  5. 善用合成层 能走合成的动画(transform/opacity)更平滑,但也别在主线程塞太多 JS 抢占帧预算。


一句话总结

可以出现:同一帧里跑了多个宏任务、做了多次样式/布局计算,但没有渲染原因是浏览器只在特定“渲染机会”才可能提交像素,且有权跳过绘制。 对策:读写分离、用 rAF 对齐、切片重计算、控制任务与微任务密度。

简短结论:不完全是。 浏览器想要“提交一帧像素”,通常需要主线程在那一刻是空闲的(至少要从 JS 执行中让出控制权),这样它才能做样式计算、布局、绘制并交给合成器。但也有少数例外,即使主线程正忙,画面依然可能变化。


更精确地说

需要主线程空闲才能发生的

  • 样式重算 / 布局 / 绘制 / DOM 变更产生的可见更新 这些步骤都在主线程完成;如果你用长 JS/连续微任务把主线程占住了,这帧就无法进入渲染阶段(会延后或掉帧)。

  • requestAnimationFrame 回调 rAF 在渲染前触发,也需要主线程有空;主线程被占用时,rAF 会被推迟

  • 微任务清空 渲染前浏览器会跑一轮(或多轮)microtasks(如 Promise.then/queueMicrotask)。如果你不停塞微任务,可能出现微任务饥饿,导致渲染一直被拖后。

不一定需要主线程空闲也会动的

  • 合成器线程驱动的变化(Compositor-only)

    • 典型:只改 transform / opacity 的 CSS 动画/过渡 这类动画很多情况下由合成器线程推进,即使主线程短时忙碌,视觉仍可能继续(但主线程太久不让也会卡输入/事件)。
    • 硬件加速的视频播放、GIF 在某些实现中也可能继续推进帧。
    • 异步滚动/惯性滚动 在现代浏览器常由合成器参与,主线程忙时滚动可能仍继续(交互响应会受影响)。

所以更准确的表述是:大多数 DOM/CSS 带来的可见更新需要主线程阶段性空闲;但合成器可独立推进某些“只合成”的动画与效果。


实用建议

  • 长任务切片:把重计算拆成小段(setTimeout(0) / scheduler.postTask / requestIdleCallback / Web Worker),给浏览器留出渲染机会。
  • 避免微任务饥饿:不要在 then/queueMicrotask 里无限自我追加。
  • 动画尽量走合成器:能用 transform/opacity 就别改会触发布局/绘制的属性;必要时配合 will-change
  • 读写分离:同一宏任务中先批量读布局,再批量写样式,减少强制同步布局。

一句话:渲染不要求“完全没有 JS”,但要求主线程在渲染机会点上能被抢到。主线程若被 JS 长时间占用,渲染就会被推迟;只有合成器可独立推进的少数场景能在此期间继续“看起来在动”。

本站总访问