1. 渲染机制
1.1. doctype
DTD (document type definition,文档类型定义) 告诉浏览器文档类型,浏览器再决定用什么引擎解析渲染之
doctype 告诉浏览器 当前 DTD(当前使用的文档类型)
常见的 doctype 类型:
- html5:<!DOCTYPE>
- html4.01 strict:不包括废弃标签如 font
- html4.01 transitional 过渡版本
1.2. 浏览器渲染过程
- 首先,解析 HTML Source,构建 DOM Tree
- 同时,解析 CSS Style,构建 CSSOM Tree
- 然后,组合 DOM Tree 与 CSSOM Tree,去除不可见元素,构建 Render Tree,同时进行 Layout
- Painting
- Composition
- Display
页面变化重新渲染时:
- 再执行 Reflow,根据 Render Tree 计算每个可见元素的布局(几何属性)
- 最后,执行 Repaint,通过绘制流程,将每个像素渲染到屏幕上。
在第三步,在建立 Render Tree 时(WebKit 中的「Attachment」过程),浏览器就要为每个 DOM Tree 中的元素根据 CSS 的解析结果(Style Rules)来确定生成怎样的 renderer。对于每个 DOM 元素,必须在所有 Style Rules 中找到符合的 selector 并将对应的规则进行合并。选择器的「解析」实际是在这里执行的,在遍历 DOM Tree 时,从 Style Rules 中去寻找对应的 selector。
所以,css 解析顺序: 从右到左进行查找。如果从左到右的顺序,那么每条选择器都需要遍历整个 DOM 树,性能很受影响。所谓高效的 CSS 就是让浏览器在查找 style 匹配的元素的时候尽量进行少的查找, 所以选择器最好写的简洁一点。
Layout 告诉 render tree 每个元素位置,宽高等信息。
Painting: This is the process of filling in pixels. It involves drawing out elements.
Compositing: parts of the page were drawn into potentially multiple layers they need to be drawn to the screen in the correct order so that the page renders correctly. (组合:多层显示顺序)
For more information: https://developers.google.com/web/fundamentals/performance/rendering/?hl=en
1.2.1. 重排 reflow
页面字体大小改变或者元素移动位置等,浏览器需要重新计算每个元素盒子模型的位置。这个过程叫 reflow。回炉(重新塑形)。举个例子,页面上节点是以树的形式展现的。假如我使用 JavaScript 砍掉一个节点,这棵树为了不脱节,肯定要重新梳理一遍,将砍掉的那个断点重新结合起来又形成一颗完整的树,而这个结合梳理过程就是这里的 reflow,所谓回流,就是由于某些原因(如修改),要将元素回过头来重新“流”一遍
触发 reflow:
- 增删改 DOM 结点
- 移动 DOM 位置、动画
- css 样式 display, height 等改变
- resize scroll 有可能
- 修改网页字体(不要这样做,性能问题)
- 特殊:offset、scroll、clientX、getComputedStyle、currentStyle:
由于浏览器在处理批量修改页面元素样式时,会将批量操作缓存起来,然后再做一次 reflow 过程(异步 reflow),避免每次操作都执行 reflow 消耗资源。但是如果在某个上述特殊操作之后立马调用了以上执行属性,为了等够得到最新的样式,会检查缓存的操作,是否需要 reflow,这样就 flush 出最新的样式。
如何减少 reflow ?
- 不轻易增删改 DOM,不要修改网页字体,不轻易移动 DOM 等。
- 少用
display:none
而使用visibility:hidden
- 减少不必要的 DOM 深度。改变 DOM 节点树上任何一个层级都会影响从根结点一直到修改的子节点。
- 精简 css,去除没有用处的 css
- 如果你想让复杂的表现发生改变,例如动画效果,那么请在这个流动线之外实现它。使用 position-absolute 或 position-fixed 来实现它。
- 避免不必要的复杂的 css 选择符,尤其是使用子选择器,或消耗更多的 CPU 去做选择器匹配。
- 减少样式的重新计算,即减少 offset、scroll、client*、getComputedStyle、currentStyle 的使用,因为每次调用都会刷新操作缓冲区,执行 reflow & repaint。
- 避免
window.onresize
1.2.2. 重绘 repaint
repaint happens when you change the look of an element without changing the size and shape. This doesn't cause reflow as geometry of the element didn't changed.
触发 repaint:
- change background color
- change text color
- visibility hidden
DOM 改变,如添加了新元素,reflow repaint 都发生。
如何尽量减少 repaint 频率:减少页面颜色改变。最后一次性添加结点,而不要每次操作 DOM 都立马修改 DOM。
1.2.3. 注意
Render Tree 只包含渲染网页所需要的节点 Reflow 过程是布局计算每个对象的精确位置和大小 Repaint 过程则是将 Render Tree 的每个像素渲染到屏幕上。
1.3. 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?(重要)
从 URL 规范、HTTP 协议、DNS、CDN、数据库查询、到浏览器流式解析、CSS 规则构建、layout、repaint、onload/DOMContentLoaded、JS 执行、JS API 绑定等等
- 在浏览器地址栏输入 URL
- 浏览器查看 强缓存 是否命中
- 如果资源未缓存,发起新请求
- 如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
- 检验新鲜通常有两个 HTTP 头进行控制
Expires
和Cache-Control
:- HTTP1.0 提供
Expires
,值为一个绝对时间表示缓存新鲜日期 - HTTP1.1 增加了
Cache-Control
: max-age=,值为以秒为单位的相对过期时间
- HTTP1.0 提供
- 浏览器 解析 URL 获取协议,主机,端口,path。并 组装一个 HTTP(GET)请求报文
- 浏览器 获取主机 ip 地址,过程如下:通过 DNS 解析获取网址
- 浏览器缓存
- 本机缓存
- hosts 文件
- 路由器缓存
- ISP DNS 缓存
- 通过 DNS 解析获取网址 递归查询(可能存在负载均衡导致每次 IP 不一样)
- 打开一个 socket 与目标 IP 地址、端口建立 TCP 链接,三次握手如下:
- 客户端发送一个 TCP 的SYN=1,Seq=X的包到服务器端口 (客户端:我要和你连接)
- 服务器发回SYN=1, ACK=X+1, Seq=Y的响应包(服务端:行啊,你发个约定指令给我,我就跟你继续通信)
- 客户端发送ACK=Y+1, Seq=Z(客户端:我按照约定发这个指令了)
- TCP 链接建立后 发送 HTTP 请求
- 服务器接受请求并解析,将请求转发到服务程序
- 服务器检查 HTTP 请求头是否包含协商缓存验证信息(Last-Modified, Etag),如果验证缓存新鲜,返回304等对应状态码
- 处理程序读取完整请求并准备 HTTP 响应,可能需要查询数据库等操作
- 服务器将响应报文通过 TCP 连接发送回浏览器
- 浏览器接收 HTTP 响应,然后根据情况选择关闭 TCP 连接或者保留重用,关闭 TCP 连接的四次握手如下:
- 主动方发送 Fin=1, Ack=Z, Seq= X (服务器:我给你发送结果了)
- 报文被动方发送 ACK=X+1, Seq=Z (客户端:我收到结果了)
- 报文被动方发送 Fin=1, ACK=X, Seq=Y (客户端:请关闭链接吧)
- 报文主动方发送 ACK=Y, Seq=X 报文 (服务器:行,我关闭了哈)
- 浏览器检查响应状态码:是否为 1XX,3XX, 4XX, 5XX,这些情况处理与 2XX 不同
- 如果资源可缓存,进行缓存
- 对响应进行解码(例如 gzip 压缩)
- 根据资源类型决定如何处理(假设资源为 HTML 文档)
- 解析 HTML 文档,构件 DOM 树,下载资源,构造 CSSOM 树,Render Tree,Layout, 执行 js 脚本,这些操作没有严格的先后顺序,以下分别解释
- 构建 DOM 树:
- Tokenizing:根据 HTML 规范将字符流解析为标记
- Lexing:词法分析将标记转换为对象并定义属性和规则
- DOM construction:根据 HTML 标记关系将对象组成 DOM 树
- 解析过程中遇到图片、样式表、js 文件,启动下载
- 构建CSSOM 树:
- Tokenizing:字符流转换为标记流
- Node:根据标记创建节点
- CSSOM:节点创建 CSSOM 树
- 根据 DOM 树和 CSSOM 树构建渲染树:
- 从 DOM 树的根节点遍历所有可见节点,不可见节点包括:1)
script
,meta
这样本身不可见的标签。2)被 css 隐藏的节点,如display: none
- 对每一个可见节点,找到恰当的 CSSOM 规则并应用
- 发布可视节点的内容和计算样式
- 从 DOM 树的根节点遍历所有可见节点,不可见节点包括:1)
- js 解析如下:
- 浏览器创建 Document 对象并解析 HTML,将解析到的元素和文本节点添加到文档中,此时 document.readyState 为 loading
- HTML 解析器遇到没有 async 和 defer 的 script 时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用 document.write() 把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作 script 和他们之前的文档内容
- 当解析器遇到设置了 async 属性的 script 时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用 document.write(),它们可以访问自己 script 和之前的文档元素
- 当文档完成解析,document.readState 变成 interactive
- 所有 defer 脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用 document.write()
- 浏览器在 Document 对象上触发 DOMContentLoaded 事件
- 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState 变为 complete , window 触发 load 事件
- reflow, repaint, 显示页面(HTML 解析过程中会逐步显示页面)
- 浏览器会开启一个线程来处理这个请求,对 URL 分析判断如果是 http 协议就按照 Web 方式来处理;
- 调用浏览器内核中的对应方法,比如 WebView 中的 loadUrl 方法;
- 通过 DNS 解析获取网址的 IP 地址,设置 UA 等信息发出第二个 GET 请求;
- 进行 HTTP 协议会话,客户端发送报头(请求报头);
- 进入到 web 服务器上的 Web Server,如 Apache、Tomcat、Node.JS 等服务器;
- 进入部署好的后端应用,如 PHP、Java、JavaScript、Python 等,找到对应的请求处理;
- 处理结束回馈报头,此处如果浏览器访问过,缓存上有对应资源,会与服务器最后修改时间对比,一致则返回 304;
- 浏览器开始下载 html 文档(响应报头,状态码 200),同时使用缓存;
- 文档树建立,根据标记请求所需指定 MIME 类型的文件(比如 css、js),同时设置了 cookie;
- 页面开始渲染 DOM(这里继续说),JS 根据 DOM API 操作 DOM,执行事件绑定等,页面显示完成。