Skip to content

Commit 4870f4d

Browse files
committed
new file: _posts/2017-08-15-browser-render-frame.md
1 parent 777b3c9 commit 4870f4d

File tree

5 files changed

+190
-5
lines changed

5 files changed

+190
-5
lines changed

_layouts/blog.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ <h1>
2727
<div class="content">
2828
<div class="md">
2929
{{ content }}
30-
<p>转载请注明来源: <a href="http://harttle.com{{page.url}}">http://harttle.com{{page.url}}</a></p>
30+
<p>转载请注明来源: <a href="http://harttle.com{{page.url}}">http://harttle.com{{page.url}}</a> 欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论(可能需要在能访问 disqus 服务的网络),也可以邮件至 <a href="mailto:harttle@126.com">harttle@126.com</a></p>
3131
</div>
3232

3333
<div class="hidden-xs">

_posts/2017-04-04-using-http-cache.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ Web 服务器(比如 Tomcat、Apache、Virgo)或服务器端框架(比如
3838

3939
# Cache-Control
4040

41-
`Cache-Control` 在 HTTP 响应头中,用于指示代理和 UA 使用何种缓存策略。
42-
比如`no-cache`为不可缓存、`private`为仅 UA 可缓存,`public`为大家都可以缓存。
41+
`Cache-Control` 在 HTTP 响应头中,用于指示代理和 UA 使用何种缓存策略(感谢 [Kingsley Chen](http://kingsamchen.github.io/) 指出此前对 no-cache 描述的错误)。比如:
42+
43+
* [no-cache][no-cache] 为本次响应不可直接用于后续请求(在没有向服务器进行校验的情况下)
44+
* [no-store][no-store] 为禁止缓存(不得存储到非易失性介质,如果有的话尽量移除,用于敏感信息)
45+
* `private`为仅 UA 可缓存
46+
* `public`为大家都可以缓存。
4347

44-
> The Cache-Control general-header field is used to specify directives that MUST be obeyed by all caching mechanisms along the request/response chain. --14.9, [RFC 2616][2616]
4548

4649
`Cache-Control`为可缓存时,同时可指定缓存时间(比如`public, max-age:86400`)。
4750
这意味着在 1 天(60x60x24=86400)时间内,浏览器都可以直接使用该缓存(此时服务器收不到任何请求)。
@@ -162,3 +165,5 @@ Cache-Control:max-age=0
162165
[vary]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/vary
163166
[cond]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Conditional_requests
164167
[best-practice]: /2017/04/04/http-cache-best-practice.html
168+
[no-cache]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
169+
[no-store]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
---
2+
title: 浏览器的 16ms 渲染帧
3+
tags: DOM JavaScript 异步 性能 重绘
4+
---
5+
6+
由于现在广泛使用的屏幕都有固定的刷新率(比如最新的一般在 60Hz),
7+
在两次硬件刷新之间浏览器进行两次重绘是没有意义的只会消耗性能。
8+
浏览器会利用这个间隔 16ms(1000ms/60)适当地对绘制进行节流,
9+
因此 16ms 就成为页面渲染优化的一个关键时间。
10+
尤其在[异步渲染][async-render]中,要利用 [流式渲染][css-js-render] 就必须考虑到这个渲染帧间隔。
11+
12+
## TL;DR
13+
14+
为方便查阅源码和相关资料,本文以 Chromium 的 [Blink][blink] 引擎为例分析。如下是一些分析结论:
15+
16+
* 一个渲染帧内 commit 的多次 DOM 改动会被合并渲染;
17+
* 耗时 JS 会造成丢帧;
18+
* 渲染帧间隔为 16ms 左右;
19+
* 避免耗时脚本、交错读写样式以保证流畅的渲染。
20+
21+
<!--more-->
22+
23+
# 渲染帧的流程
24+
25+
渲染帧是指浏览器一次完整绘制过程,帧之间的时间间隔是 DOM 视图更新的最小间隔。
26+
由于主流的屏幕刷新率都在 60Hz,那么渲染一帧的时间就必须控制在 16ms 才能保证不掉帧。
27+
也就是说每一次渲染都要在 16ms 内页面才够流畅不会有卡顿感。
28+
这段时间内浏览器需要完成如下事情:
29+
30+
* 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等
31+
* 样式计算(CSS Object Model):级联地生成每个节点的生效样式。
32+
* 布局(Layout):计算布局,执行渲染算法
33+
* 重绘(Paint):各层分别进行绘制(比如 3D 动画)
34+
* 合成(Composite):合成各层的渲染结果
35+
36+
最初 Webkit 使用定时器进行渲染间隔控制,
37+
2014 年时开始 [使用显示器的 vsync 信号控制渲染][remove-timer](其实直接控制的是合成这一步)。
38+
这意味着 16ms 内多次 commit 的 DOM 改动会合并为一次渲染。
39+
40+
# 耗时 JS 会造成丢帧
41+
42+
JavaScript 在并发编程上一个重要特点是“Run To Completion”。在事件循环的一次 Tick 中,
43+
如果要执行的逻辑太多会一直阻塞下一个 Tick,所有异步过程都会被阻塞。
44+
一个流畅的页面中,JavaScript 引擎中的执行队列可能是这样的:
45+
46+
```
47+
执行 JS -> 空闲 -> 绘制(16ms)-> 执行 JS -> 空闲 -> 绘制(32ms)-> ...
48+
```
49+
50+
如果在某个时刻有太多 JavaScript 要执行,就会丢掉一次帧的绘制:
51+
52+
```
53+
执行很多 JS...(20ms)-> 空闲 -> 绘制(32ms)-> ...
54+
```
55+
56+
```html
57+
<div id="message"></div>
58+
<script>
59+
var then = Date.now()
60+
var i = 0
61+
var el = document.getElementById('message')
62+
while (true) {
63+
var now = Date.now()
64+
if (now - then > 1000) {
65+
if (i++ >= 5) {
66+
break;
67+
}
68+
el.innerText += 'hello!\n'
69+
console.log(i)
70+
then = now
71+
}
72+
}
73+
</script>
74+
```
75+
76+
# 测量渲染帧间隔
77+
78+
浏览器的渲染间隔其实是很难测量的。即使通过 [clientHeight][client-size] 这样的接口也只能强制进行Layout,是否 Paint 上屏仍未可知。
79+
80+
幸运的是,最新的浏览器基本都支持了 [requestAnimationFrame][requestAnimationFrame] 接口。
81+
使用这个 API 可以请求浏览器在下一个渲染帧执行某个回调,于是测量渲染间隔就很方便了:
82+
83+
```javascript
84+
var then = Date.now()
85+
var count = 0
86+
87+
function nextFrame(){
88+
requestAnimationFrame(function(){
89+
count ++
90+
if(count % 20 === 0){
91+
var time = (Date.now() - then) / count
92+
var ms = Math.round(time*1000) / 1000
93+
var fps = Math.round(100000/ms) / 100
94+
console.log(`count: ${count}\t${ms}ms/frame\t${fps}fps`)
95+
}
96+
nextFrame()
97+
})
98+
}
99+
nextFrame()
100+
```
101+
102+
每次 `requestAnimationFrame` 回调执行时发起下一个 `requestAnimationFrame`,统计一段时间即可得到渲染帧间隔,以及 fps。逼近 16.6 ms 有木有!
103+
104+
![render frame](/assets/img/blog/dom/render-frame.gif)
105+
106+
# 渲染优化建议
107+
108+
现在我们知道浏览器需要在 16ms 内完成整个 JS->Style->Layout->Paint->Composite 流程,那么基于此有哪些页面渲染的优化方式呢?
109+
110+
## 避免耗时的 JavaScript 代码
111+
112+
耗时超过 16ms 的 JavaScript 可能会丢帧让页面变卡。如果有太多事情要做可以把这些工作重新设计,分割到各个阶段中执行。并充分利用缓存和懒初始化等策略。不同执行时机的 JavaScript 有不同的优化方式:
113+
114+
* 初始化脚本(以及其他同步脚本)。对于大型 SPA 中首页卡死浏览器也是常事,建议增加服务器端渲染或者应用懒初始化策略。
115+
* 事件处理函数(以及其他异步脚本)。在复杂交互的 Web 应用中,耗时脚本可以优化算法或者迁移到 Worker 中。Worker 在移动端的兼容性已经不很错了,可以生产环境使用。
116+
117+
## 避免交错读写样式
118+
119+
在编写涉及到布局的脚本时,常常会多次读写样式。比如:
120+
121+
```javascript
122+
# 触发一次 Layout
123+
var h = div.clientHeight
124+
div.style.height = h + 20
125+
# 再次触发 Layout
126+
var w = div.clientWidth
127+
div.style.width = w + 20
128+
```
129+
130+
因为浏览器需要给你返回正确的宽高,上述代码片段中每次 Layout 触发都会阻塞当前脚本。
131+
如果把交错的读写分隔开,就可以减少触发 Layout 的次数:
132+
133+
```javascript
134+
# 触发一次 Layout
135+
var h = div.clientHeight
136+
var w = div.clientWidth
137+
div.style.height = h + 20
138+
div.style.width = w + 20
139+
```
140+
141+
## 小心事件触发的渲染
142+
143+
我们知道 [DOM 事件的触发][dispatchEvent] 是异步的,但事件处理器的执行是可能在同一个渲染帧的,
144+
甚至就在同一个 Tick。例如异步地获取 HTML 并拼接到当前页面上,
145+
通过监听 XHR 的 [onprogress 事件][onprogress] 来模拟流式渲染:
146+
147+
```javascript
148+
var xhr = new XMLHttpRequest(),
149+
method = 'GET',
150+
url = 'http://harttle.com'
151+
152+
xhr.open(method, url, true)
153+
xhr.onprogress = function () {
154+
div.innerHTML = xmlhttp.responseText
155+
};
156+
xhr.send()
157+
```
158+
159+
上述渲染算法在网络情况较差时是起作用的,但不代表它是正确的。
160+
比如当 <http://harttle.com> 对应的 HTML 非常大而且网络很好时,
161+
`onprogress` 事件处理器可能碰撞在同一个渲染帧中,或者干脆在同一个 Tick。
162+
这样页面会长时间空白,即使 `onprogress` 早已被调用过。
163+
164+
> 关于异步渲染的阻塞行为,可参考 <http://harttle.com/2016/11/26/dynamic-dom-render-blocking.html>
165+
166+
# 参考链接
167+
168+
* [Thinking in Animation Frames: Tuning Blink for 60 Hz][thinking-60]
169+
* [The Blink Project][blink]
170+
* [Issue: Remove style recalc timer][remove-timer]
171+
172+
[remove-timer]: https://bugs.chromium.org/p/chromium/issues/detail?id=337617
173+
[blink]: https://chromium.googlesource.com/chromium/blink/
174+
[thinking-60]: https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/bxIPxpzLprQ
175+
[async-render]: http://harttle.com/2016/11/26/dynamic-dom-render-blocking.html
176+
[css-js-render]: http://harttle.com/2016/11/26/static-dom-render-blocking.html
177+
[client-size]: /2016/04/24/client-height-width.html
178+
[requestAnimationFrame]: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
179+
[dispatchEvent]: https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/dispatchEvent
180+
[onprogress]: https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequestEventTarget/onprogress

assets/img/blog/dom/render-frame.gif

40.6 KB
Loading

bin/generate_tags.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# usage: generate_tags.sh xxx.md
33

44
# generate tag list
5-
grep name ./_site/tags.json | awk -F : '{print $2}' | tr -d ',\" ' > /tmp/tags.txt
5+
grep name ./_site/api/tags.json | awk -F : '{print $2}' | tr -d ',\" ' > /tmp/tags.txt
66

77
# match tag string
88
grep $1 -oFf /tmp/tags.txt | sort | uniq | tr '\n' ' ' | sed 's/ $//'; echo ''

0 commit comments

Comments
 (0)