HeadlinesBriefing favicon HeadlinesBriefing.com

浏览器主线程性能详解与优化指南

Hacker News •
×

当你听到“前端优化”时,脑海中浮现的是什么?对我们大多数人来说,是减少网络请求、压缩包体积,或是善用缓存。除此之外,可能还包括减少重渲染,或调整资源加载时机。主线程通常不会被提及,这是有原因的:在大多数屏幕上,它从未成为问题。但在交互密集、数据实时流入、滚动、动画与输入交织在一起的页面上,情况就不同了。无论你在网络和包体积上节省了多少,一旦主线程被阻塞,屏幕就会冻结。你大概遇到过这样的网站:滚动偶尔卡顿、按钮响应稍慢、在搜索框输入的字符半拍才显示出来。这不至于让人恼火,却以一种微妙的方式让人心烦。这种卡顿就是主线程被阻塞的样子。作为开发者,当我们遇到这类卡顿时,通常的反应是怀疑“会不会是我的代码太慢?”,继而开始拆解算法或寻找多余的计算。但在大多数情况下,代码的速度并不是问题。代码并不慢。它只是恰好占用了主线程。浏览器有多个线程,但我们能从代码层面触及的几乎所有东西都集中在主线程上。计算、渲染、事件处理、网络响应处理、以及框架内部机制,全都在这里处理。一个资源,堆积如山的工作。浏览器的主线程很昂贵。大多数时候它不惹麻烦,但一旦你想做些有野心的事情,应对主线程就成了关键。本文将探讨如何驾驭这一昂贵资源。我们先来看看主线程到底做什么。它的工作大致分为两类。第一类是运行 JavaScript。我们编写的代码、事件处理器、定时器、网络响应回调、以及框架内部机制,全都在这里运行。这些任务按进入队列的顺序,在有空隙时执行,与屏幕刷新周期无关。第二类是绘制屏幕。当 DOM 或样式发生变化、屏幕需要更新时,浏览器会按大致以下顺序经历这些步骤来生成一帧。运行 requestAnimationFrame 回调 — 注册在帧绘制前运行的 JavaScript 样式计算 — 计算每个元素的最终 CSS 值 布局 — 计算每个元素的位置和大小(也称为回流) 绘制 — 生成绘制指令,描述在何处用什么颜色绘制什么 如果没有变化,这些步骤会被完全跳过,所以它们不一定每帧都运行。只有最后的合成步骤,将生成的输出组装到屏幕上,会被交给合成器线程。换句话说,绘制屏幕管道的前半部分大多是主线程的责任。用于更新屏幕的渲染管道 为了让屏幕看起来流畅,帧必须以显示器的刷新率绘制。在最常见的 60Hz 显示器上,这意味着每秒 60 帧,即每帧约 16.6 毫秒。而且你无法用完所有时间。扣除浏览器自身的处理开销后,实际预算通常被认为是每帧 10 毫秒左右,而在 120Hz 设备上,预算本身就减半了。问题在于,上述两类工作在同一线程上排成单行。JavaScript 设计之初采用的是单线程事件循环模型。主线程一次处理一个任务,当该任务运行时,其他什么也做不了。如果一个 JavaScript 函数运行 200 毫秒,那么在这 200 毫秒里,浏览器既无法重绘屏幕,也无法接收用户的点击。相对于约 10 毫秒的帧预算,这是致命的时长。一个运行时间过长的任务会...