Appearance
结论先行(TL;DR)
会的。 浏览器只有在“渲染机会(rendering opportunity)”点才可能提交像素;在此之前:
- 可以连续执行多个宏任务(Tasks);
- 在这些任务里可以多次触发布局/回流计算(包括你代码里强制布局的同步读取);
- 但仍然不发生绘制提交(像素不更新)。 原因:JS 执行期间不会绘制,浏览器有权跳过这一帧的绘制(无可见变化/后台/节流/时间片不够/微任务饥饿等)。
一帧内的“可能”时间线(简化版)
Task A(你的脚本/事件/定时器回调)
↳ 多次写样式/读布局(可能触发样式/布局计算)
Microtasks(Promise/queueMicrotask 等)
Task B(例如另一个 setTimeout/postMessage 回调)
↳ 再次写样式/读布局(再次触发布局计算)
Microtasks(再次)
requestAnimationFrame(若文档可见)
Microtasks(有的实现会在 rAF 后再跑一次)
渲染机会(样式→布局→绘制→合成)← 这一步**可能**被跳过
requestIdleCallback(若本帧还有空闲)关键点:渲染机会在一串任务与微任务之后才出现;浏览器可根据负载与可见性跳过绘制。
为什么能出现“多次计算、不渲染”
JS 主线程正在跑任务 浏览器不会中断 JS 去绘制。你在 Task 里频繁
offsetHeight/getComputedStyle等,会强制样式/布局计算,但这些只是计算步骤,不是像素提交。多个任务连着跑 比如密集的
setTimeout(0)、postMessage/MessageChannel、事件风暴(滚轮、网络、I/O)。在下一次“渲染机会”到来前,可能已经跑了多个 Task。浏览器可以跳过绘制 无可见变化、标签页后台、节流/省电、超时长任务导致帧预算不足、或连续微任务造成的微任务饥饿,都会让这一帧的绘制被跳过。
合成优化不等于每次绘制 某些变更(如
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 提交少。
实战建议(避免“算了很多却没绘制”)
读写分离 在同一 Task 内,先批量读取布局,再批量写入样式,避免交替触发强制回流。
用
requestAnimationFrame对齐渲染节奏- 渲染相关写操作(尤其动画步进)放进 rAF;
- 读操作可在 rAF 开头统一读取,写操作在 rAF 末尾统一写入。
控制任务密度
- 不要滥用
setTimeout(0)/postMessage连环调用; - 长计算用切片(
setTimeout/scheduler.postTask/requestIdleCallback)或搬到 Web Worker。
- 不要滥用
避免微任务饥饿 连续
queueMicrotask/Promise.then循环会推迟渲染;必要时插入一个宏任务切片。善用合成层 能走合成的动画(
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 长时间占用,渲染就会被推迟;只有合成器可独立推进的少数场景能在此期间继续“看起来在动”。