本系列其他译文请看JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)
本章阅读指数:5
本章跟日常工作相关性很强,实用性也很强,阅读的难度也不高。建议阅读
迄今为止,我们主要讨论的是JS语言的特性。
但是当构建App时,代码是需要跟它所处的环境进行交互的。理解环境的原理和构成,能让你构建更好的app,也能为发布之后潜在的问题做好准备
来看看浏览器环境的主要组成吧:
-
用户接口: 包含地址栏,前进/返回的按钮,书签等等。基本上是你能在浏览器上看到的每一个部分,除了网页本身所在是窗口
-
浏览器具引擎: 处理用户接口和原先引擎之间的交互
-
渲染引擎: 渲染网页显示.渲染引擎解析了HTML和CSS文件,并把解析的内容显示到屏幕
-
网络:这里是网络调用(比如XHR请求),这些网络调用是基于跨平台的接口实现的
-
后台UI:用来绘制核心部件,比如沙箱和视窗。后台暴露了无平台声明的接口,底层是使用了操作系统的UI方法。
-
JS引擎:代码执行的地方,之前第5章讨论过。
-
数据持久化: 你的App或许需要在本地存储数据,支持的存储机制包括 localStorage, indexDB, WebSQL and FileSystem.
这一章,我们会研究一下渲染引擎,因为它是处理了HTML和CSS的解析和可视化,这些是大多数的 JavaScript 应用需要持续进行交互的方面。
渲染引擎总览
渲染引擎的主要责任是,在浏览器中显示请求的页面。 渲染引擎可以显示HTML,XML,图像。如果你使用了额外的插件,浏览器也可以显示其他类型的文档比如PDF。
渲染引擎
跟JS引擎类似,不同的浏览器使用了不同的渲染引擎。这里是一些流行的:
- Gecko — Firefox
- WebKit — Safari
- Blink — Chrome, Opera (from version 15 onwards)
渲染流程
渲染引擎接受网络层传递请求文档的内容。
构建DOM树
渲染引擎是第一步是解析HTML文档,然后转换被解析的元素到DOM 树真实的DOM 节点
假如有文本要输入:
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="theme.css">
</head>
<body>
<p> Hello, <span> friend! </span> </p>
<div>
<img src="smiley.gif" alt="Smiley face" height="42" width="42">
</div>
</body>
</html>
这个HTML的DOM树长这样:
每一个元素都是直接包含的所有元素的父节点。
构建CSSOM树
CSSOM 是CSS Object Model的简称。当浏览器构建页面DOM时,它在head区域遇到了一个link 标记指向外部theme.css 脚本。引擎猜测需要渲染页面的资源,它立刻发送一个请求。 假如theme.css文件长这样
body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
和HTML一起,引擎需要转换CSS到CSSOM--这样可以和浏览器一起工作。CSSOM长这样:
为什么CSSOM是一个树结构?当计算一个节点在页面上的最终样式时,浏览器先按照节点的一般规则(例如,如果节点是body元素的子节点,那么会应用所有的body样式),然后通过页数的会泽来递归优化计算过的样式。
看看我们给出的案例。body元素中span中包含的任意文本,尺寸都是16px,都是红色。这些样式是继承body元素的。如果span元素是p元素的子节点,那么它的内容是不会显示的,因为给他制定了更加明确的样式。(display:none)
注意上面的树不是一个完全的CSSOM树,只是显示了我们在样式脚本中重写的。每一个浏览器都提供了一个默认的样式集,也就是用户代理样式---当我们什么都不写时,看到的样式就是它提供的。案例中的样式只是简单的重写了他们的默认样式。
构建渲染树
HTML 中的可视化指令和 CSSOM 树的样式数据结合起来创建渲染树。
渲染树是一个可视化元素构建的树,按照它们将在平面上显示的顺序构建。它是HTML和对应的CSS的视觉表征。树的意义是让网页内容能在页面上按照正确的顺序绘制。 渲染树中每一个节点,在Webkit中都是一个渲染器或者渲染对象。
上面提到的DOM和CSSOM树的渲染树是这样子的:
为了构建渲染树,浏览器大概做了以下动作
- 从DOM树的根部开始,遍历每一个可视节点。一些节点是不可视的(比如,脚本标记,meta标记等等),因为他们无法在渲染树中映射,所以被忽略了。这些节点通过样式隐藏然后也会被忽略。比如以上例子中的 span 节点,因为为其显式设置了
display: none的样式。 - 对每一个可视的节点,浏览器找到合适的CSSOM规则,然后应用它们。
- 释放出包含内容及其经过计算的样式的可见节点
你可以看一下RenderObject的源码: github.com/WebKit/webk…
让我们看看这个类的核心元素
class RenderObject : public CachedImageClient {
// Repaint the entire object. Called when, e.g., the color of a border changes, or when a border
// style changes.
Node* node() const { ... }
RenderStyle* style; // the computed style
const RenderStyle& style() const;
...
}
每一个渲染器表示一个矩形区域,通常是一个节点的CSS盒。它包含了几何信息,包含宽,高和位置。
渲染树的布局
当一个渲染器创建,添加到树中,它没有位置和尺寸信息。计算这些值得过程就是布局。
HTML使用一个流布局的模型,大多时间它可以一次性计算几何信息。坐标系统是相对于根渲染器的。这里使用 Top 和 left 坐标。
布局是一个递归的过程,它从根节点开始--对应于HTML文档的<html>元素。布局持续递归整个或者整个渲染器层级结构,为每个需要计算几何信息的渲染器计算其信息。
根渲染器的位置是0,0,它的标注的大小就是浏览器视窗可见部分(也就是视口)的尺寸。
启动布局程序,意味着给出每一个节点在屏幕上应该显示的具体位置。
绘制渲染树
在这个阶段,遍历渲染树,调用渲染器的paint()把内容现实到屏幕上。
绘制是全局或者渐进式的(类似布局)
- 全局 —整个树被渲染.
- 渐进式 — 只有一些渲染器改变,而不会影响整个树。渲染器让它所处的矩形区域失效,这引起了操作系统认为整个区域需要重绘,并生成一个
paint事件。操作系统会很机智的把多个区域合并成一个。
理解绘制是渐进流程是很重要的。为了获取更好的UX,渲染引擎将会尽快显示内容到屏幕上。它不会等待所有的HTML解析之后去构建和布局渲染树。会优先解析和显示部分内容,与此同时持续处理从网络接收的剩下的内容项。
脚本和样式的处理顺序
当转换到一个<script>标签,脚本立刻被解析和执行。文档转换被暂停,直到脚本执行完。这意味这过程是同步
如果脚本是外部的,首先需要从网络中fetch(同步),在获取完成之前所有的解析都会停止
HTML5增加了一个选项去比较脚本为异步的,这样就可以在另一个线程中解析和执行。
优化渲染性能
如果要优化你的APP,这里有5个主要的区域你可以关注一下:
- JavaScript -前面我们讨论过如何优化JS,同时不阻塞UI。渲染时,我们需要思考JS代码和DOM元素交互的方式。JS会创建很多UI的改变,尤其在SPA中。
- 样式计算 —这个过程,决定了你的元素会使用什么样的CSS规则。一旦规则定义了,他们会被应用和计算。
- 布局 —一旦浏览器知道元素要用什么规则,就开始计算元素需要占用多少空间,以及在浏览器屏幕中的位置。网页布局模型规定了,一个元素可以影响其他的。比如,
<body>的宽度可以影响它所有子节点的宽度。这就导致,布局就算是计算密集型的。绘制会在多层中完成。 - 绘制 —这里是纹理被填充的地方。绘制文本,颜色,图像,边框,阴影等等,元素的每一个可视的部分。
- 合成 — 由于页面部分是在多层中绘制的,他们需要有序的在屏幕上绘制,才能正确的渲染页面。这个很重要,尤其是重叠元素。
优化JS
浏览器中JS经常视觉变化,尤其在SPA中。
为了提升渲染,你可以做以下一些优化:
- 避免用
setTimeout或者setInterval进行视觉更新。他们将会在帧的任意时间调用callback,很可能在帧末尾。我们期望的是在帧开始时去触发视觉改动,这样不会丢帧。 - 将长时间的JS运算放到WebWorker中,我们第三章讨论过。
- 使用微任务处理跨帧的DOM改变。 这是为了预防当任务需要访问 DOM,而网络工作线程无法办到的情况的。这意味你需要拆分一个大任务到几个晓得任务中,然后根据不同的任务性质在
requestAnimationFrame,setTimeout,setInterval中运行。
优化CSS
增加或者删除元素,改变属性等等都会让浏览器重新计算样式,重新布局整个或者部分页面。
优化渲染,可以这么做:
- 降低选择器的复杂性。复杂的选择器会占用超过50%的时间开计算元素样式,剩下的时间才用来构建样式。
- 减少必须产生样式计算的元素的个数。本质上,直接更改少数元素的样式而不是使整个页面的样式失效。
优化布局
对浏览器来说,布局重计算是非常重的。可以这样优化:
- 尽可能减少布局数。当样式改变时,浏览器都会去插件是否需要布局重新计算。改变一些几何信息比如宽,高,位置等都会需要布局,所以尽可能减少改变这些属性。
- 使用
flexbox替代老的布局方式,它的性能更好。 - 拒绝强制同步布局。需要谨记,当JS运行时,前一帧的所有旧布局值都是已知且可查询的。此时使用
box.offsetHeight不会有问题,但是如果你在访问前就改变了box的样式(比如给元素动态添加了CSS类)。浏览器将首先应用新的样式,然后调用布局。这个就非常耗时耗资源,尽力避免这样做。
优化绘制过程
这经常是最长时间运行的认为,所以尽量避免这么做。我们可以这样优化:
- 改变除了透明度和transfroms的其他任意属性,都会引起绘制。所以谨慎使用。
- 如果触发了布局,也会触发绘制,因为改变几何信息会引起元素的视觉改变
- 通过提升层和动画编排来减少绘制区域。