浏览器如何渲染页面
在地址栏输入 url 后发生了什么?
用一张图描述一下:
- 输入域名
- DNS:域名解析,得到 ip 地址
- 建立 TCP 连接
- 客户端发起请求
- 服务端处理请求,返回数据
- 断开 TCP 连接
- 浏览器拿到请求结果,解析 HTML,渲染页面
优化方案里,和日常开发息息相关的是最后的流程,浏览器解析 html,渲染页面。本文主要介绍这一块内容。
网页进程:浏览器网页的渲染和 JS 执行在一个单独的进程中执行。这个进程也称为render 进程。每启动一个页面,都会启动一个 render 进程。
说到网页进程,又引入了线程的概念。
如果说进程是一个工厂,那线程就是工厂中的流水线。一个工厂的正常运行,往往需要多个流水线通力合作才能完成。在网页进程中也是一样,想要网页进程能正常渲染运行,也需要多个线程参与合作。
网页的渲染运行,有如下线程参与
GUI 渲染线程
GUI(Graphical User Interface)表示图形用户界面的意思,render tree 的渲染。用一张图看一下渲染路径
渲染路径
后端返回的 html 文件其内容是个字符串,<html>、<style>
等标签是语法糖,HTML Parser 或 CSS Parser 表示对应的解析器,解析器的工作就是将这些字符串语法糖转换为对象。
加载:表示『资源文件发起请求->服务器返回结果』这一过程
解析:表示词法分析,先加载,后解析
渲染:将节点信息绘制到页面的过程
html 的解析过程,是自上而下的,解析过程中,如果发现了样式文件(link 标签),就会加载样式文件。样式文件的加载并不会阻塞 html 的解析,阻塞的是 render tree 的渲染。
HTML 会解析出 DOM(Document Object Model)树,样式文件会解析出 CSSOM(CSS Object Model)树,在 Attachment 环节,GUI 线程将 DOM 树与 CSSOM 树合并在一起,生成渲染树(render tree),并将渲染树绘制到页面。
为什么样式文件的加载会阻塞 render tree 的渲染?
如果不会阻塞,页面会先出现没有样式的 DOM 结构,等 CSSOM 树生成后,需要重新计算 Render Tree,这会造成没必要的损耗。
用代码证明 css 的阻塞现象:
我们先把下载速度设置为 20kbit/s
开发者工具-> Network -> No throttling -> 添加一个 20kbit/s,再观察结果
1 |
|
怎么解决 css 阻塞的阻塞现象?我们可以提高 css 的加载速度:
- 使用 CDN。CDN 会替你挑选最近的节点为你提供资源。
- 对 css 进行压缩。webpack,gulp 等打包工具
- 将多个 css 文件合并。
思考:在页面中新增一个弹窗节点,加在哪个位置最好?页面是如何重新渲染的?
加在 DOM 结构末尾。无论放在哪个位置,整个 HTML 都会重新解析,加在末尾是为了减少生成 render tree 时的计算过程(整颗新旧树还是会比较的,react diff 也会同理)
JS 与渲染路径
js 与样式文件:
解析 HTML 时,如果在 script 元素之前发现样式文件,样式文件的加载和解析会阻塞 JS 的解析和执行。
用代码证明 css 的加载阻塞 js 的执行。
1 |
|
可以看出,bootstrap 样式文件加载了 111964ms,加载完成后才执行了后面的 JS。
JS 与 HTML,用一张图来表示:
稍微解释一下这张图:
解析 HTML 时,如果发现了 script 元素,JS 的加载和解析都会阻塞 HTML 的解析,等 script 加载以及执行完毕再解析 HTML。这就是我们将 script 放在 DOM 结构后面的原因。
那是不是说 script 标签必须放在 DOM 结构后面?并不是,因为可以给 script 标签添加 async 或 defre 属性。
带 async 属性的 script:
async 表示异步,即用异步的方式加载脚本,render 进程会开启一个新的线程来加载 JS,所以JS 的加载不会阻塞 HTML 的解析,但 JS 的执行仍然会阻塞 HTML 的解析。等 JS 执行完毕后,才继续解析 HTML。
因为 async 加载完成之后会马上执行,所以它是无序的。
带 defer 属性的 script:
defer 表示延迟,即延迟执行,像 async 一样,JS 的加载不会阻塞 HTML 的解析,并且 JS 会在 HTML 解析完成后再执行。
async 与 defer 的区别:
- async 脚本是无序的,适合‘完全不依赖它或它不被任何脚本依赖’的脚本
- async 脚本的执行会阻止 HTML 的解析,defer 不会
async 与 defer 的相同点:
- 使用 async 或 defer 属性,render 进程会开启一个新的线程来加载 JS 文件,也就是说 script 的加载是和 HTML 的解析同时进行的
HTML 的解析能不能和 JS 的执行同时进行?
不能。GUI 线程负责 HTML 的解析,JS 引擎线程负责 JS 的执行,虽然是两个线程,但他们是互斥的。
补充:
DOMContentLoaded
HTML 解析完成后,DOMContentLoaded 就会触发,解析完成表示我们已经可以访问页面的 DOM 元素了。DOMContentLoaded 类似于 JQuery 中的$(document).ready(function() { // ...代码... })
。
load
整个页面及所有依赖资源如图片和样式表都已加载完成时触发。
DOMContentLoaded 时间会比 load 小,时间差表示依赖资源加载的时间。
参考:
https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html
https://segmentfault.com/q/1010000000640869
https://www.cnblogs.com/caizhenbo/p/6679478.html
在页面的交互过程中,需要重绘或者由于某些操作引发了回流时,GUI 会重新计算 render tree,然后重新绘制页面元素。
重绘和回流
重绘(repaint):当页面元素的样式发生改变时,GUI 会根据新样式重新绘制该元素,这个过程称为重绘。重绘不影响布局
导致重绘的操作有哪些:
- 改变元素的外观,可见性(visibility)
- 回流导致重绘
利用 chrome 中的 Paint flashing 工具,可以观察元素的重绘。
开发者工具 -> more tools -> Rendering,勾选 Paint flashing。重绘的元素会高亮显示。
回流(reflow):当页面元素的尺寸、结构发生改变时,GUI 重新计算的过程,这个过程称为回流。回流完成后,重新绘制受影响的部分到屏幕中,所以说回流必将引起重绘。
导致回流的操作有哪些:
- 浏览器窗口发生变化
- 元素的尺寸、位置发生变化
- 元素内容、字体大小发生变化
- 激活 css 伪类,例如 hover
- 添加或者删除元素
- 滚动
如何减少回流
抓住一个核心原则,对于 DOM 元素的操作,避免影响该元素之外的元素
- 如果想要改变元素样式,可以通过 class 名,而不是使用 JS 操作
- 避免使用多层内联样式
- 首屏服务端渲染,减少页面内容计算次数
- 避免使用 table 布局
JS 引擎线程
浏览器是不支持直接运行 JS 代码的,所以需要在浏览器中植入一个内核,来支持 JS 的解析和运行。在 chrome 中,这个内核叫 V8。
一个网页只会启动一个 JS 线程来处理 JS 脚本。
JS 线程是单线程。这点不难理解,如果是多线程,一个线程删除 DOM,一个线程新增 DOM,浏览器要如何处理呢?
还有一点,JS 线程和 GUI 线程是互斥的。所以在渲染路径阶段,存在阻塞问题。
定时触发器线程
专门负责 setTimeout/setInterval 的逻辑。应该结合事件循环中的队列来理解定时器线程的执行过程。
事件触发线程
当我们鼠标点击与滑动、键盘的输入等都会触发一些事件,而这些事件的触发逻辑的处理,就是依靠事件触发线程来帮助浏览器完成。
该线程也会把事件的逻辑放入队列中,等待 JS 引擎的处理。在事件循环中,事件触发为宏任务。
http 线程
使用无状态短链接的 http 请求,在应用层基于 http 协议的基础之上,达到与服务端进行通信的目的。
该线程的触发罗家,不是在 JS 引擎线程之中,过程是异步的。
参考:
https://juejin.cn/post/6844903667733118983
https://juejin.cn/post/6844903667733118983