优化关键渲染路径

浏览器渲染原理


image
image

浏览器渲染流程

JavaScript


JavaScript实现动画效果,DOM元素操作等。

CSSOM


确定每个DOM元素应该应用什么CSS规则。

注意: CSS选择器越详细,匹配工作越多,匹配节点越慢。

RenderTree(渲染树)


RenderTree包含了渲染网页所需的节点, 无需渲染的节点不会被添加到RenderTree中。 如:,display: none的节点

注意:因为设置了visibility:hidden的元素虽不可见,但仍然占有空间,仍会被添加到RenderTree。

Layout(布局)


计算每个DOM元素在最终屏幕上显示的大小和位置。由于web页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫reflow。

注意:影响web性能的一个重要的问题就是repaint和reflow。

触发Layout

  • 屏幕旋转
  • 浏览器视窗改变
  • 与大小位置相关的CSS属性改变

Paint(绘制)


根据background,border,box-shadow等样式,将Layout生成的区域填充为最终将显示在屏幕上的像素

Composite(渲染层合并)


按照合理的顺序合并图层然后显示到屏幕上。

三种渲染流程


实际场景下,大概会有三种常见的渲染流程:

  • JavaScript -> CSS -> Layout -> Paint -> Composite
  • JavaScript -> CSS -> Paint -> Composite
  • JavaScript -> CSS -> Composite

    注意:Layout和Paint步骤是可避免的

优化CSS


浏览器会在DOM和CSSOM加载完开始渲染页面。

避免CSS阻塞初次渲染


1
2
<style>/* styles here */</style>
<link rel="stylesheet" href="index.css">

通过以上两种方式定义的CSS,均会阻塞初次渲染。浏览器会在解析完CSS后,再进行渲染。这是为了防止样式突变带来的抖动。通过link标签引入的CSS阻塞的时间可能更长,因为加载它需要一个网络来回时间

media query

1
<link rel="stylesheet" href="index_print.css" media="print">

此样式表仍会加载。当浏览器环境不匹配媒体查询条件时,该样式表不会阻塞渲染。我们可针对不同媒体环境拆分CSS文件,并为link标签添加媒体查询,避免为了加载非关键CSS资源,而阻塞初次渲染

1
2
3
4
var style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'index.css';
document.head.appendChild(style);

该方法不会阻塞初次渲染。

preload

1
<link rel="preload" href="index_print.css" as="style" onload="this.rel='stylesheet'">

rel不是stylesheet,因此不会阻塞渲染。preload是resoure hint规范中定义的一个功能,resource hint通过告知浏览器提前建立连接或加载资源,以提高资源加载的速度。浏览器遇到遇到标记为preload的link时,会开始加载,当onload事件发生时,将rel改为stylesheet,即可应用此样式。

总结

引入CSS资源的方法 是否阻塞初次渲染
通过document.write写入以上标签
通过DOM API插入HTMLLinkElement对象
使用preload方式载入CSS
为link添加media query 当媒体查询不匹配时,不会阻塞

减少需要执行样式计算的元素个数


由于浏览器的优化,现代浏览器的样式计算直接对目标元素执行,而不是对整个页面执行,所以我们应该尽可能减少需要执行样式计算的元素的个数。

JavaScript优化


避免Javascript阻塞HTML Parser(解析器)


1
2
3
4
5
<-- inline js -->
<script>/* app logics here */</script>
<-- external js -->
<script src="somescript.js"></script>

通过以上两种方式引入js均会阻塞HTML parser,因而会阻塞出现在脚本后面的HTML标记的渲染。而外部script阻塞的时间一般更长,因为可能包含了一个网络来回时间。

Javascript可以通过document.write修改HTML文档流,因此在执行js时,浏览器会暂停解析DOM的工作。

CSS阻塞JS

1
2
3
4
5
<-- inline js -->
<script>/* app logics here */</script>
<-- external js -->
<script src="somescript.js"></script>

通过以上两种方式引入的JS均会被CSS阻塞,由于这些Javascript可能会读取或修改CSSOM,因此需等待CSSOM构造完成后,它们才能执行

将资源放到文档底部,延迟js执行

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head></head>
<body>
<h1>世界上最美丽的语言是什么?</h1>
<button>See answer</button>
<!-- index.js内容:
为button标签添加点击事件,点击后,alert答案
-->
<script src="index.js"></script>
<!-- 百度统计代码 -->
<script src="tongji.js"></script>
</body>
</html>

使用defer延迟脚本执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<!-- index.js内容:
为button标签添加点击事件,点击后,alert答案
-->
<script src="index.js" defer></script>
<!-- 百度统计代码 -->
<script src="tongji.js" defer></script>
</head>
<body>
<h1>世界上最美丽的语言是什么?</h1>
<button>See answer</button>
</body>
</html>

当script标签拥有defer属性时,该脚本会被推迟到整个HTML文档解析完后,再开始执行。被defer的脚本,在执行时会严格按照在HTML文档中出现的顺序执行

注意: 使用defer时,浏览器会保证脚本按照在文档中出现的顺序执行

使用async异步加载脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<!-- index.js内容:
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('p').onclick=function() {
alert('surprise')
}
});
-->
<script src="index.js" async></script>
<!-- 百度统计代码 -->
<script src="tongji.js" async></script>
</head>
<body>
<p>Hello World</p>
</body>
</html>
  • 当script标签拥有async属性时,该脚本不会再阻塞HTML parser。且不会被CSS阻塞。
  • 脚本只要加载完成,便可开始执行。
  • 被async的脚本,在执行时会不会严格按照在HTML文档中出现的顺序执行
  • async适用于无依赖的独立资源

image

总结

引入JS资源的方法 是否阻塞文档内容初次渲染
在head中引入外部脚本或内联脚本
将脚本放到body底部
为脚本添加defer属性
为脚本添加async属性

用requestAnimationFrame代替setTimeout或setInterval


setTimeout(callback)和setInterval(callback)无法保证callback函数的执行时机,很可能在帧结束的时候执行,从而导致丢帧。requestAnimationFrame(callback)可以保证callback函数在每帧动画开始的时候执行。

image

帧丢失

用Web Worker去处理耗时的JS代码


JavaScript代码运行在浏览器的主线程上,与此同时,浏览器的主线程还负责样式计算、布局、绘制的工作,如果JavaScript代码运行时间过长,就会阻塞其他渲染工作,很可能会导致丢帧。

每帧的渲染应该在16ms内完成,但在动画过程中,由于已经被占用了不少时间,所以JavaScript代码运行耗时应该控制在3-4毫秒。
如果真的有特别耗时且不操作DOM元素的纯计算工作,可以考虑放到Web Workers中执行。

1
2
3
4
5
6
7
8
9
10
11
var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
// 主线程不受Web Workers线程干扰
dataSortWorker.addEventListener('message', function(evt) {
var sortedData = e.data;
// Web Workers线程执行结束
// ...
});

用多个frame去处理DOM元素的更新


由于Web Workers不能操作DOM元素的限制,所以只能做一些纯计算的工作,对于很多需要操作DOM元素的逻辑,可以考虑分步处理,把任务分为若干个小任务,每个任务都放到requestAnimationFrame中回调执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var nextTask = taskList.pop();
// 执行小任务
processTask(nextTask);
if (taskList.length > 0) {
requestAnimationFrame(processTaskList);
}
}

Layout优化


避免触发布局


当修改了元素的属性之后,浏览器将会检查为了使这个修改生效是否需要重新计算布局以及更新渲染树,对于DOM元素的“几何属性”修改,比如width/height/left/top等,都需要重新计算布局。

使用flexbox替代老的布局模型


老的布局模型以相对/绝对/浮动的方式将元素定位到屏幕上。Floxbox布局模型用流式布局的方式将元素定位到屏幕上。
通过一个小实验可以看出两种布局模型的性能差距,同样对1300个元素布局,浮动布局耗时14.3ms,Flexbox布局耗时3.5ms
image

其他优化


Font


Font阻塞内容渲染

  • 浏览器为了避免FOUT(Flash Of Unstyled Text),会尽量等待字体加载完成后,再显示应用了该字体的内容
  • 只有当字体超过一段时间仍未加载成功时,浏览器才会降级使用系统字体。每个浏览器都规定了自己的超时时间
  • 但这也带来了FOIT(Flash Of Invisible Text)问题。内容无法尽快地被展示,导致空白。