[译]深入了解现代web浏览器(三)
本文是根据Mariko Kosaka在谷歌开发者网站上的系列文章https://developer.chrome.com/blog/inside-browser-part3/ 翻译而来,共有四篇,该篇是第三篇。对于其中一些直译出来不太好理解的句子,笔者做了加工处理和提炼。
渲染器进程的内部工作
在前面我们介绍了多进程架构和导航流程,在这篇文章中我们将看看渲染器进程内部发生了什么。
渲染器进程涉及到web性能的许多方面。渲染器进程内部运行的细节非常之多,而本篇文章只是做一个概览;如果你想要深入了解,在这里可以找到更多资源。
渲染器进程处理web内容
渲染器进程负责响应tab页面中发生的所有事情。在渲染器进程中,绝大部分代码都是由主线程处理;如果你使用了web worker或是service worker,则你的Javascript代码是由工作者线程(worker thread)来处理。合成器和光栅化线程也是运行在渲染器进程中的——以此让页面更加高效、流畅地渲染。
渲染器进程的核心工作就是将HTML,CSS和Javascript转换为能够让用户交互的web页面。
渲染器进程内部包括了主线程、工作者线程、合成器线程和光栅化线程
解析
DOM的构建
当渲染器进程接收到导航的提交消息并开始接收HTML数据时,主线程开始解析文本字符串(HTML)并将其转换为文档对象模型(DOM)。
DOM是浏览器对页面的内部表示,也是web开发人员可以通过JavaScript与之交互的数据结构和API。
将HTML文档解析为DOM由HTML标准定义。您可能已经注意到,将HTML提供给浏览器解析永远不会引发错误。例如,缺少</p>
结束标记也是有效的HTML。像Hi!<b>I'm <i>Chrome</b>!</i>
(b标签在i标签之前关闭)这样的错误标记会被视为Hi!<b>I'm <i>Chrome</i></b><i>!</i>
。这是因为HTML规范被设计为是可以优雅地处理这些错误的。如果您对这些事情是如何完成的感到好奇,您可以阅读HTML规范的解析器中的错误处理和奇怪案例简介部分。
子资源加载
一个网页通常还会引用外部资源像图片,CSS和Javascript。这些文件需要从网络或者缓存中加载。主线程会在解析构建DOM的过程中找到它们并一一请求。但为了加快这一过程,会同时运行一个“预加载扫描器”;如果HTML文档中有类似于<img>
或<link>
这样的标签,预加载扫描器会在HTML解析器生成对应的token时找到它并向浏览器进程中的网络线程发送请求,因此这些子资源的加载不会影响到DOM树的解析构建(下面会讲到Javascript资源在大多数情况下是会影响到DOM解析构建的)。
主线程解析HTML并构建DOM树
Javascript会阻塞解析
当HTML解析器遇到一个<script>
标签时,它就不得不停止对HTML文档的解析,转而去加载、解析和执行Javascript代码。为什么呢?因为Javascript可以只用诸如document.write()
的代码来改变整个DOM的结构(HTML标准中的overview of the parsing model小节处的示意图很好地描述了这一过程)。这就是为什么HTML解析器不得不等待Javascript运行完后才能继续解析HTML文档。如果你对Javascript执行中发生的事情感兴趣,可以阅读V8团队的JavaScript engine fundamentals
提示浏览器如何加载资源
web开发人员有多种方式来给浏览器提示,以便更好地加载资源。如果你的Javascript代码中没有使用诸如document.write()
这样会改变DOM结构的API,可以添加async
或者defer
属性到<script>
标签中;然后浏览器会异步地加载和运行该Javascript代码,不会阻塞HTML解析。如果合适地话,你也可以使用Javascript模块。
样式计算
拥有DOM不足以知道页面会是什么样子,因为我们还会在CSS中设置页面元素的样式。主线程解析CSS并确定每个DOM节点的计算样式;这里做的事情主要就是关于如何根据CSS选择器为每个元素计算正确的样式信息。你可以在Devtools中的computed(计算样式)部分查看某个元素上应用的所有样式(位于Elements板块的右侧)。
主线程解析CSS并添加计算样式
即使你不提供任何的CSS,每个DOM节点也会有计算样式。例如h1
标签就比h2
标签显示的更大、每个元素都会定义有外边距margin
。这是因为浏览器有一个默认样式表。如果你想知道Chrome的默认CSS是什么样的,可以查看这里的源代码。
布局
现在渲染器进程知道了HTML文档的结构以及每个节点的样式,但这还不足以渲染出一个页面。想象一下,你正在通过电话向你的朋友描述一幅画:“这里有一个红色的大圆形和一个蓝色的小正方形”,这样并没有足够的信息让你的朋友确切地知道这幅画是什么样的。
一个人站在一幅画的前面,与另一个人通话
布局是一个获取元素几何信息的过程。主线程遍历DOM树和计算样式(computed style),创建出包含xy坐标和边界框大小等信息的布局树。布局树和DOM树的结构类似,但它只包含页面上可见内容的相关信息;如果应用了display: none
,那么该元素就不会成为布局树的一部分(然而应用visibility: hidden
的元素是会在布局树中的,详情可见这里)。类似地,如果应用了诸如p::before{content: "Hi!"}
这样的伪类,即使其生成的内容不在DOM中,仍然会被包含在布局树种。
主线程使用计算样式遍历DOM树并生成布局树
确定一个页面的布局是一项非常有挑战性的任务。即使是从上到下的块(block)流这种简单的布局,也必须要考虑字体的大小以及在哪里换行,因为这些会影响到段落的大小和形状,然后影响到下一行的段落会在哪里。
段落因为换行而移动的盒布局
CSS可以使元素浮动到一侧、掩盖移除的内容,亦或是改变书写方向;可以想象,布局阶段的任务非常艰巨。在Chrome中,有一整个工程师团队专门负责布局的工作。如果你想了解这块工作的细节,可以观察BlinkOn Conference上一些被记录下来且非常有趣的演讲。
绘制
拥有DOM,样式和布局还是不足以渲染出页面。假设你正在复现一幅画,你知道了元素的大小,形状和位置,但你还需要决定绘制它们的顺序。
一个人拿着笔刷站在画布前,想知道该先画圆还是先画正方形
比如,可能为某些元素设置了z-index
属性;在这种情况下,按照元素在HTML中的顺序来绘制将会导致错误的渲染:
页面元素按照HTML标签的顺序出现导致了错误的渲染,因为没有把z-index考虑进去
在此绘制步骤中,主线程遍历布局树并创建绘制记录。绘制记录是对“先画背景,再是文字,再然后是矩形”这样的绘画过程的记录。如果你使用过Javascript在canvas
元素上绘制,你应该会对此过程感到熟悉。
主线程遍历布局树并创建绘制记录
更新渲染管线(pipeline)的成本很高
在渲染管线中需要掌握的最重要的一点是,在每一步骤中,利用上一次操作的结果来创建新的数据。例如,如果布局树中的某些内容发生了变化,则需要为文档中受影响的部分重新生成绘制顺序。
DOM+样式,布局树和绘制记录的生成顺序
如果你为元素设置了动画,那么浏览器将会在每帧之间执行上述的操作。我们大多数的显示器都是一秒钟刷新60次(60fps)。当你在每一帧中都有在屏幕上移动物体的时候,该动画对于人眼来说会显得很平滑。但是,如果动画在某些帧中错失了的话,页面就会表现得"janky"(美式俚语,表示质量不好不可靠的意思)。
在时间轴上的动画帧
即使你的渲染操作跟得上屏幕的刷新频率,但由于这些计算是发生在主线程上的,这就意味着:它会被你的运行中的Javascript给阻塞住(我们在前面提到过,Javascript也是运行在主线程上的):
时间轴上的动画帧,但其中好几帧都被Javascript阻塞了
你可以将Javascript的执行拆分为小块,利用requestAnimationFrame()
这一API来安排它们运行在每一帧中。有关此主题的更多信息,请看Optimize JavaScript Execution。你还可以通过在Web worker中运行Javascript来避免主线程的阻塞。
在带有动画帧的时间轴上运行更小的Javascript块
合成
你会怎么画一个页面
现在浏览器知道了文档的结构,每个元素的样式,页面的几何信息以及绘制顺序,接下来它会如何画出这个页面呢?将这些信息转换为屏幕上的像素的操作被称为光栅化。
一种简单的光栅化过程的示意动画
处理这个问题的一种简单直接的办法就是——只光栅化视窗内的部分。如果用户滚动了页面,则移动已光栅化过的帧,接着光栅化缺失的部分。这是Chrome最开始发布时所采用的处理方法。然而,现代浏览器的光栅化过程更为复杂,被称为——合成。
什么是合成
合成是一项将页面各个部分分成多个层,再将它们单独地进行光栅化并最终在合成线程中合成为一个完整页面的技术。如果发生了滚动,由于各个图层都已经光栅化了,因此需要做的只有将它们合成为一个新的帧。可以通过相同的方式,即移动图层然后合成新帧的方式来实现动画。
合成过程的示意动画
你可以通过DevTools中的Layers面板来查看你的页面是如何被划分为多个图层的。
划分图层
为了找出每个元素需要被划入哪个图层,主线程遍历布局树来创建图层树(这部分在DevTools中的performance面板中被称为“更新图层树”)。如果页面中某一部分应该被划为单独的图层(比如侧边滑动栏)却并没有如此,你可以在CSS中使用will-change
属性来提示浏览器这么做。
主线程遍历布局树来生成图层树
你可能会想给每一个元素都设为单独的图层,但是跨多图层进行合成可能会比我们最开始讲述的方法(在每帧中光栅化页面中的某一部分)还要更慢;因此测量你的应用程序的性能是至关重要的。有关该主题的更多信息,请参阅坚持使用仅与合成器相关的属性和管理图层数量。
主线程的光栅与合成
一旦图层树被创建并且绘制顺序确定了,主线程会将这些信息提交至合成线程,然后合成线程光栅化每一个图层。一个图层可能会很大甚至是同整个页面一样大,所以合成线程会将它们分成小块并将每个小块发送到多个光栅线程中。光栅线程光栅化每个小块并将它们存放到GPU内存中。
光栅线程为每个小块创建位图(bitmap)并发送到GPU中
合成线程可以针对不同的光栅线程按优先级排序处理,因此可以让视窗中(或者视窗附近)的内容优先处理。一个图层还会有针对不同分辨率的图块来处理诸如放大之类的动作。
图块在光栅化完成后,合成线程会收集被称为draw quads的图块信息以此来创建合成帧。
draw quads | 包含图块在内存中的位置以及当合成页面时图块应该绘制的地方等信息 |
合成帧 | 用来表示当前页面的一帧画面的draw quads集合 |
合成帧之后会通过IPC提交到浏览器进程。与此同时,也会有来自其他地方的合成帧被提交;例如为了改变浏览器UI的UI线程,或者是其他为插件服务的渲染进程。这些合成帧会被发送到GPU以显示在屏幕上。如果一个滚动事件到来,合成线程会继续创建合成帧再发送给GPU。
合成线程创建合成帧。该帧被发送到浏览器进程然后再到GPU
采用合成方式的好处是可以从主线程中分离开来:合成线程不需要等待样式计算或者Javascript执行(因为在合成线程中单独计算完了图层需要展示的内容和位置),这也是为什么以合成方式来运行动画被认为是获取流畅表现的最佳方式。如果需要重新计算布局或者绘制,则必须涉及到主线程。
总结
在本篇文章中,我们了解到了从解析到合成的整个渲染管线。希望你现在能够阅读更多有关于网页性能优化的内容。
在下篇文章也就是最后一篇文章中,我们将深入合成线程中的更多细节,看看当用户输入产生mouse move
和click
事件时都发生了些什么。
[译]深入了解现代web浏览器(三)的更多相关文章
- [译]36 Days of Web Testing(三)
Day 14: Automate the tedious Why ? 有些时候,web测试还是蛮单调乏味的,在开始测试前,你可能要必须跳转到一个特定的表单页面,或则为了得到一个特定的页面(或配置),你 ...
- C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志
C#实现多级子目录Zip压缩解压实例 参考 https://blog.csdn.net/lki_suidongdong/article/details/20942977 重点: 实现多级子目录的压缩, ...
- React Native 项目运行在 Web 浏览器上面
React Native 的出现,让前端工程师拥有了使用 JavaScript 编写原生 APP 的能力.相比之前的 Web app 来说,对于性能和用户体验提升了非常多. 但是 React Nati ...
- web—第三章XHTML
web—第三章XHTML 又是一周 我们学的了做表单:一开始我以为表单是表格.但结果:表单是以采集和提交用户输入数据的,这样讲很迷,说简单点就是登陆端.比如:Facebook.twitter.Ins ...
- 第十一章:WEB浏览器中的javascript
客户端javascript涵盖在本系列的第二部分第10章,主要讲解javascript是如何在web浏览器中实现的,这些章节介绍了大量的脚本宿主对象,这些对象可以表示浏览器窗口.文档树的内容.这些章节 ...
- [C# 网络编程系列]专题四:自定义Web浏览器
转自:http://www.cnblogs.com/zhili/archive/2012/08/24/WebBrowser.html 前言: 前一个专题介绍了自定义的Web服务器,然而向Web服务器发 ...
- web浏览器中javascript
1.异步载入一个js代码function loadasync(url) { var head = document.getElementsByTagName("head")[0]; ...
- SeaJS:一个适用于 Web 浏览器端的模块加载器
什么是SeaJS?SeaJS是一款适用于Web浏览器端的模块加载器,它同时又与Node兼容.在SeaJS的世界里,一个文件就是一个模块,所有模块都遵循CMD(Common Module Definit ...
- 转:【专题四】自定义Web浏览器
前言: 前一个专题介绍了自定义的Web服务器,然而向Web服务器发出请求的正是本专题要介绍的Web浏览器,本专题通过简单自定义一个Web浏览器来简单介绍浏览器的工作原理,以及帮助一些初学者揭开浏览器这 ...
- JavaScript权威指南--WEB浏览器中的javascript
知识要点 1.客户端javascript window对象是所有客户端javascript特性和API的主要接入点.它表示web浏览器的一个窗口或窗体,并且可以用window表示来引用它.window ...
随机推荐
- IoT技术的最后决战!百万大奖究竟花落谁家?
2022年5月25日华为云IoT创新应用开发大赛决赛路演正式打响! 华为云IoT创新应用开发大赛是华为云面向IoT产业领域的重量级精品赛事,自去年11月上线以来,受到了物联网协会.生态伙伴.产业基地等 ...
- 如何用Xcode安装ipa
Xcode安装ipa iOS APP上架App Store其中一个步骤就是要把ipa文件上传到App Store! 下面进行步骤介绍! 利用Appuploader这个软件,可以在Windows.L ...
- 开心档之MySQL 数据类型
MySQL 数据类型 MySQL 中定义数据字段的类型对你数据库的优化是非常重要的. MySQL 支持多种类型,大致可以分为三类:数值.日期/时间和字符串(字符)类型. 数值类型 MySQL 支持所有 ...
- 火山引擎DataLeap如何解决SLA治理难题(一):应用场景与核心概念介绍
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 基于火山引擎分布式治理的理念,数据平台数据治理团队自研了火山引擎DataLeap SLA保障平台,目前已在字节内部 ...
- 以平安银行“智能化银行3.0”实践,看火山引擎DataTester如何助推金融行业数智化进程
作者:DataTester 银行业正在进入一场围绕客户为中心的新革命时期.流量红利逐渐消失,银行零售进入存量客户精细化经营时代:"互联网+"给金融带来更多的场景,智能化成为零售 ...
- Python 3.12 抢先看——关于 f-string 的改动
Python 3.12 抢先看--关于 f-string 的改动 哈喽大家好,我是咸鱼 相信小伙伴们对 python 中的 f-string 都不陌生 f-string 是格式化字符串的缩写,是以小写 ...
- 【算法学习笔记】区间DP
基本的知识点引用自 OI wiki,感谢社区的帮助 什么是区间 DP? 区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系.令 ...
- AIsing Programming Contest 2020 游记 (ABC水题,D思维)
补题链接:Here A - Number of Multiples 水题 B - An Odd Problem 水题 C - XYZ Triplets 水题,注意数组不要开小了 D - Anythin ...
- 单线程 Redis 如此快的 4 个原因
本文翻译自国外论坛 medium,原文地址:https://levelup.gitconnected.com/4-reasons-why-single-threaded-redis-is-so-fas ...
- C#设计模式03——简单工厂的写法
什么是C#简单工厂? C#简单工厂是一种创建对象的设计模式,它定义一个工厂类来创建指定类型的对象,而不是在客户端代码中直接创建对象.简单工厂模式通常使用静态方法来生成对象,并且这些静态方法通常被称为工 ...