图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?
Posted 承香墨影
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?相关的知识,希望对你有一定的参考价值。
Chrome 算是程序员的标配了,从全球的市场份额来看,它在全球市场的份额已经超过 60%。
在 Chrome 10 周年之际,官方发布了一个系列文章,用图解的方式,很清晰的讲解了现代浏览器的运行原理。
本文是该系列的第三篇,为了便于阅读,我做了简单修改和注释,建议顺序阅读:
。
。
老规矩,觉得本文有帮助,就点赞、留言并转发分享吧,你喜欢总要让我知道吧!
渲染器进程的内部工作原理
本系列分为 4 个部分,主要讲解关于现代浏览器的运行原理,本文为该系列的第 3 篇。在之前的文章中,我们介绍了和,而在这篇文章中,我们将探究在渲染器进程的内部,到底发生了什么。
渲染器进程涉及到 Web 性能相关的多个方面,由于渲染器进程中处理了很多的逻辑,不是一篇文章可以全面讲解的,因此本文仅作为一个概述。如果你有兴趣深入研究,可以在《Why Performance Matters》这篇文章里找到更多的资料。
渲染器进程处理Web内容
所有选项卡内发生的逻辑,都由渲染器进程负责。在渲染器进程中,主线程处理了服务器发送给用户的大部分代码。如果你使用到 Web Workder 或者Service Worker,那 javascript 中的这部分代码,将由工作线程处理。Compositor(合成器) 和 Raster(光栅) 线程也在渲染器内运行,从而实现高效、流畅的渲染页面。
渲染器进程的核心工作是将 html,CSS 和 JavaScript 转换为用户可以与之交互的网页。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/0823a39ed034490dabd4aa4869fa847c.jpg)
上图中,描述了具有主线程、工作线程、Compositor 线程、Raster 线程的渲染器进程,以及他们之间的关系。
解析
构建 DOM
当渲染器进程收到一个导航请求,并开始接收 HTML 数据,主线程将开始处理文本字符串(HTML),将其解析成 DOM(Document Object Model)。
DOM 是 Web 页面的内部的逻辑树文档结构,Web 开发人员可以通过 JavaScript 脚本与之交互数据,以及通过标准 API 来操作 DOM 节点。
将 HTML 文档解析成 DOM 是完全依照于 HTML 协议。并且在 HTML 协议中,浏览器不会对错误的 HTML 进行错误提示。例如,缺少结束的 </p>
标签时,这依然是一个有效的 HTML。类似 Hi! <b>I'm <i>Chrome</b>!</i>
中,b
标签在 i
标签之前关闭这样的错误,会被 HTML 理解为 Hi! <b>I'm <i>Chrome</i></b><i>!</i>
。这是因为 HTML 规范的主要原则是优雅的处理这些错误,而不是严格检查。
如果你对这些规范感到好奇,可以阅读 HTML 规范中的 “解析器中的错误处理和奇怪案例介绍” 部分。
解析器中的错误处理和奇怪案例介绍:
https://html.spec.whatwg.org/multipage/parsing.html#an-introduction-to-error-handling-and-strange-cases-in-the-parser
子资源加载
一个完整的 Web 站点通常会包含图片、CSS 和 JS 等外部资源,这些文件都需要从网络或者本地缓存中加载。主线程可以在解析构建 DOM 的时候,将他们逐个请求,但是为了加快速度,会同时使用 “预加载扫描(Preload Scanner)”。
如果 “预加载扫描” 发现有类似 <img>
或 <link>
这样的标签时,会由 HTML 解析器对该资源生成一个 Tokens,然后在浏览器进程中,通过网络或者本地缓存来加载资源。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/abe8d69876cf460aa4ddc7448303e589.jpg)
上图描述了,主线程解析 HTML 并构建 DOM 树的过程。
JS 可以阻止解析
当 HTML 解析器遇到 <script>
标签的时候,它会暂停解析 HTML 文档,然后对这个 JS 脚本进行加载、解析和执行。
这么设计的原因,是因为 JS 可以使用类似 document.write()
方法来改变 DOM 的结构。这就是 HTML 解析器在重新解析 HTML 之前,必须等待 JS 脚本执行的原因。
如果你对 JS 执行中发生的事情细节有兴趣,V8 团队有一篇文章深入的对此进行了讲解,有兴趣可以看看。
V8 团队深入研究:
https://mathiasbynens.be/notes/shapes-ics
提示浏览器如何加载资源
HTML 遇到 JS 脚本则暂停对 HTML 的解析,这并不是绝对的。
Web 开发人员可以通过多种方式的配置,告知浏览器如何更优雅的加载资源。如果你的 JS 脚本中,没有使用到类似 document.write()
这样的方法,你可以在 script
标签中添加 async
或 defer
标记,然后浏览器会异步加载和运行此 JS 脚本,不会阻断解析。如果需要,也可以使用 JavaScript Modules,还可以通过 <link rel="preload">
标签向浏览器明确标记此为重要的资源,将在页面加载完成之后被立刻使用,对于这类资源,它会在页面加载生命周期的早期,被优先加载。
样式渲染(Style)
仅仅解析成 DOM,还不足以完成页面渲染,因为还可以通过在 CSS 中,设置元素的样式来丰富渲染效果。
主线程将解析 CSS,并将效果渲染到指定的 DOM 节点上,关于 CSS 选择器如何定位到指定的 DOM 节点,可以通过 DevTools 来查看相关信息。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/7185e601f42943c9a210a9a6dcfa0ab5.jpg)
即使你不使用任何 CSS 样式,每个 DOM 节点依然存在默认的渲染样式。例如,h1
标签在视觉上就大于 h2
标签,并且每个元素还有默认的边距。这是因为浏览器具有默认样式表。
如果你对 Chrome 的默认 CSS 是什么样的有兴趣,可以在源码中看到具体细节。
Chrome 的默认 CSS:
https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/css/html.css
布局(Layout)
到现在,渲染器进程知道每个 DOM 的结构和样式了,但是这依然不足以渲染页面。想象一下,你正视图通过文字向朋友描述一副画,“有一个大的红色圆圈和一个小的蓝色方块”,这些信息不足以让你的朋友还原这幅画。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/654d272e16bf41e1ab3720f2203e67c0.jpg)
这就牵扯到布局(Layout),布局是对元素定位的过程,主线程遍历 DOM 并计算样式,然后创建布局树(Layout Tree),在布局树中,包含 X、Y 坐标和边框大小等信息。布局树是一个与 DOM 树类似的结构,但是它仅仅包含了页面上可见内容相关的信息。
举个例子,如果某个元素设置了 display:none
,则该元素将不会出现在布局树中,但是它会出现在 DOM 树中,而如果该元素被设置为 visibility:hidden
则它会存在于布局树中。类似的例子还有 p::before{content:"Hi!"}
这样的伪类,它会存在于布局树中,而不会存在于 DOM 树中。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/b31613f9054f4978a23d6bf0b9899e92.jpg)
如上图所示,在主线程中渲染样式,并生成布局树和 DOM 树。
计算页面布局是一个很复杂的工作,即使最简单的从上到下的块流结构,也必须考虑字体的大小以及如何划分每一块,因为它们会影响当前段落的大小和形状,然后影响下一块所在的位置。
![dom_layout 图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/94b90965ba69432082d48359428e0607.jpg)
CSS 样式可以设置元素浮动到某一侧、隐藏 overflow 的元素,或者改变排版方向。布局是一个非常复杂的工作,在 Chrome 中,有一个完整的工程师团队负责布局。如果你的对他们工作的细节感兴趣,可以参阅 BinkOn 会议的记录。
BinkOn:
https://www.youtube.com/watch?v=Y5Xa4H2wtVA
绘制(Paint)
拥有 DOM、CSS 和 LayoutTree 仍然不足以渲染页面。假设你正在尝试重绘一幅画,你除了需要知道元素的大小、外观和位置之外,还需要知道它们的绘制顺序。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/cd6faf9120f54e8487a7315240fb57b8.jpg)
例如:z-index
属性将改变元素的层级,在这种情况下,按 HTML 中编写的元素顺序进行绘制,将导致渲染结果和预期不符。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/e85670267d6c419183476d8a62b6ebe4.jpg)
如上图所示,因为没有正确的考虑 z-index
,将导致页面被错误的渲染。
在这个绘制的过程中,主线程遍历布局树,然后创建绘制记录。绘制记录是一个绘制过程的注释,例如“背景优先,然后是文本,最后是矩形”。如果你曾经使用 JS 在 <canvas>
上绘制元素,那么你对此过程应该会很熟悉。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/14d9f29a22a24480922eb319a00571d9.jpg)
如上图所示,主线程遍历布局树,并生成绘制记录。
更新渲染管道的成本很高
渲染管道(Rendering Pipeline)中最重要的任务,就是在每个步骤开始前,根据前一次操作的结果,来创建新的数据。例如,如果布局树中的某些内容发生更改,则需要为文档的受影响部分重新生成“绘制”顺序。
渲染管道(Rendering Pipeline)中最重要的任务,就是在每个步骤开始前,根据前一次操作的结果,来创建新的数据。例如,如果布局树中的某些内容发生变动,则需要为文档中受影响的部分,重新生成“绘制记录”。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/4fca0a399ba64cafbdb819ed26ce5eb7.jpg)
为元素设置的动画,浏览器必须在每一帧之间执行这些操作。我们大多数显示器每秒刷新 60 次(60fps),如果你对每一帧都做了处理,那动画对人眼而言就是平滑的,但是如果某些帧没有被处理到或者丢失了,则会导致动画不连贯,出现页面的“卡顿”。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/f2d4f8960fab4767a7e3347d227dd258.jpg)
哪怕渲染的计算可以跟上屏幕刷新,可因为此计算过程发生在主线程上,当执行 JavaScript 脚本时,可能导致渲染过程被阻断。
即使渲染的计算可以跟上屏幕的刷新速度,可因为此计算是在主线程上执行的,这就意味着 JS 代码的执行,也可能导致它被阻断。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/d8ab3997976049f289bf1f4d9c0baed9.jpg)
如上图,时间轴上的动画帧,被 JS 阻止了一帧。
为此,你可以将 JavaScript 操作划分成小块,并在每帧上执行requestAnimationFrame()
,还可以在 Web Workers 中运行 JavaScript,以避免阻塞主线程。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/c877f46bc3f84d478dbc62bf1d71c6a0.jpg)
如图所示,在动画帧的时间轴上,运行较小的 JavaScript 块。
合成(Compositing)
如何绘制一个页面?
现在浏览器知道文档的结构,每个元素的样式,页面的形状和绘制顺序,它是如何绘制页面的?将此信息转换为屏幕上的像素称为光栅化(rasterizing)。
光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/e789fc56477741cda8b7b89246fe5a16.jpg)
也许处理这种情况的一种无脑方案,是在视口(ViewPort)内部将每个组件都光栅化。如果用户滚动页面,则移动光栅帧,并通过更多光栅元素填充缺少的部分。
这就是 Chrome 首次发布时处理光栅化的方式,但是,现代浏览器运行一个更复杂的被称为合成(Compositing)的进程。
什么是合成(Compositing)
合成是一种将页面的各个元素进行分层,分别光栅化,并在合成器线程中以一个单独的线程合成新页面的技术。如果页面发生滚动,由于图层已经光栅化,因此它需要做的就是合成一个新帧。通过移动图层同时合成新帧,可以以相同的方式实现动画。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/f3cef9b4012346f992a44900d9ecb223.jpg)
你可以在 DevTools 中的 Layout panel 来查看看图层。
分层
为了确定每个元素所在的层,主线程遍历布局树以创建层树(Layer Tree)。如果页面的某元素应该是一个单独的图层(例如侧滑菜单),那么你可以在 CSS 中,使用 will-change
属性提示浏览器。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/de9f28dd77ee49978cb1379d1fde2b7d.jpg)
如上图,在主线程中遍历布局树,并生成层树。
虽然理想情况下,应该为每个元素生成图层,但是对过多的小图层进行合并,可能会比对页面的每帧上栅格化小元素更慢,因此测量应用程序的渲染性能就非常重要。有关主题的更多信息,请参阅 Stick to Compositor-Only Properties 和 Manage Layer Count。
Stick to Compositor-Only Properties 和 Manage Layer Count:
https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count
光栅和合成,脱离主线程
一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。合成器线程会光栅化每个图层,一个图层可能想一个完整的页面那么大,因此合成器线程将他们分成图块,并将每个图块发送到光栅线程。光栅线程格式化每个元素,并将他们存储在 GPU 内存中。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/663450ef7cec4987997dadc10db5d1d9.jpg)
合成器线程可以优先考虑不同的光栅线程,以便 ViewPort(或附近)的元素可以被优先光栅化。图层还具有多个不同分辨率的倾斜度,以便对内容的放大等操作。
一旦元素被光栅化,合成器线程会收集被称为 “绘制矩形(Draw Quads)” 的信息,用以创建一个合成帧(Compositor Frame)。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/c1243b2aa54b48ff9e57083483d2f2c3.jpg)
然后通过 IPC 将合成帧提交给浏览器进程。此时,可以从 UI 线程添加另一个合成帧用于浏览器的 UI 更新,或者从其他渲染器进程中添加扩展。这些合成帧被发送到 GPU 中,用以在屏幕上显示。如果触发滚动事件,合成器线程会创建另一个合成帧发送到 GPU。
![图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?](https://image.cha138.com/20210330/96f9da7dcbb749b28871614b56e74009.jpg)
上图中,合成器线程创建合成帧。将此帧发送到浏览器进程然后发送到 GPU。
合成(Compositor)的好处,是它可以在不影响主线程的情况下完成。合成器线程不需要等待样式计算或者 JS 脚本执行,这就是为什么 “仅合成动画” 被认为是平滑性能的最佳选择。如果需要再次计算不会或者重新绘制,则必须涉及到主线程。
合成动画:
https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
小结
在这篇文章中,我们研究了从解析到合成的渲染流程,希望你现在有兴趣探究有关网站性能优化的更多内容。
在下一篇文章中,将更详细的介绍合成器线程,并解释当用户触发 mouse move
和 click
时,会发生什么。
「」
以上是关于图解Chrome:HTML/CSS/JS是如何在浏览器中,渲染成你看到的页面?的主要内容,如果未能解决你的问题,请参考以下文章