本文主要讲谈及web浏览器的渲染原理、流程以及相关的性能问题
最近在复习时遇到一个问题,关于async和defer,发现自己还能记住一点,然而再往深一想,浏览器的渲染顺序?怎么防止阻塞DOM渲染?如何保证首屏优化、关键渲染路径优化?如何从浏览器渲染、网络请求、js引擎机制优化性能?好像找不到让自己满意的答案,所以查阅资料写个博客总结一下(本文主要基于当前(2017-7)和chrome浏览器来讲的)。
async和defer是script标签的属性,其有先决条件必须src属性存在才能生效,这有两个最基本的讨论点
async是赋予脚本异步属性,其特性如下:
defer是赋予脚本延迟属性,其特性如下:
扒一下html规范里面的图,简单说一下规范的内容,首先有两种情况的script,一种是经典script(classic script),可以理解为我们最常用的、普通的script标签;另一种是模块script(module script),是一种支持度不高的规范,其代码会被当作JavaScript模块处理,不太清楚的读者可以点击链接详细看一下,这里不展开了。
先讲一下规范里经典script的处理方法,总共三种情况:
不使用async和defer,根据所在位置阻塞解析,被下载紧接着执行直到完成。
使用async,不阻塞解析并行下载脚本,当下载完成后阻塞解析立即执行,在解析完成前后都可能被执行。
使用defer,并行下载,并在页面完成解析时进行执行,不会阻塞解析
另外还补充一点,如果浏览器不支持async,那么浏览器会将其作为defer处理,也算是优雅降级,至少不会出现你把async放在head失效直接阻塞后面body的情况。
简单总结一下,两者有一些共通点
二者下载都不会阻塞DOM解析,这点还是很重要的,首屏时间就是一切
都是给外链脚本使用,不考虑请求数量而使用内联情况下等因素的首选方式
都可以使用onload事件进行一系列处理
二者最主要的不同点就是async其实只注重异步下载时不阻塞html解析,下载完毕后会立刻执行,而defer会完全保证异步下载和执行都不会阻塞html解析,这点在不同场景使用还是比较重要的。
比如async就适合基本没DOM操作,不在乎依赖的模块,而且要尽可能小或者说执行时间短,否则对首屏可能还是影响很大的;defer相对而言就规矩的多了,按序加载,不会影响html解析。
基本async和defer就是这样了,不得不感叹一下,在规范里就几行文字,很多相关解析文章都讲错讲少了,走了不少弯路,以后还是认准文档大法。
上面我们知道了async的script执行可能会阻塞解析,可是css呢?另外其他的资源如何下载?会不会阻塞解析?我们说的解析,DOM parse到底是什么?另外domContentLoaded和load分别是基于那个节点触发?这些问题我们可能要深思一下。
上图展示了浏览器渲染的过程,我们可以先忽略JavaScript的部分,然后剩下的就是一条直线了。下面从头开始说起
上图展示了从html的字节码被浏览器处理为DOM的过程
转换:根据字节的编码规则将其转化为特定字符,也就是characters
生成tokens:将character转化为w3c定义的各种特定标签 ,生成tokens(令牌)
词法解析:匹配字符串,将tokens按照规则转换为包含特定属性和规则的节点对象(nodes)
DOM构建:根据每个节点的层次关系和规则转换为直观的树形结构,具有明确的父子关系。
值得一提的是,HTML都是增量构建的,在HTML文件还在传输时html parse就可以开始了。
最终我们的到了页面完整的文档对象模型(DOM),在以后的页面渲染包括布局、绘制等都会用到它。它代表了页面的结构,决定了整个页面的初始格局,而下面的CSS对象模型(CSSOM)决定了页面的五彩斑斓。
老规矩先放图,上图是CSSOM构建流程图,跟DOM构建差不多的套路,将CSS文件的字节码转换为符合浏览器特定规则的字符,然后浏览器对其进行解析和构成树。
与DOM有所不同的是,其整个的计算过程略有复杂,包括一套复杂的特异度计算规则(CSS属性来源 -> 特异度大小 -> 书写顺序前后覆盖),最终确定每个节点的样式值形成下图的不完整CSSOM。
CSS一直被认为是一种渲染阻塞资源(所谓CSS白屏),因为渲染树是依赖CSSOM才能生成,进而走浏览器的布局渲染流程,所以我们才有了CSS放在head的最佳实践。
可见节点:渲染树包含了渲染网页所需的所有节点,不需要渲染的节点是不会合并到渲染树中的,比如元数据元素meta,base等,还有设置了display:none的节点。
布局的最终结果是一个“盒模型”,它需要精确的计算出每个元素所占据的位置坐标,将如rem、vw、em等相对测量值(计算值)转换为屏幕上的绝对像素。
将相对转换为绝对,这就需要首先明确或定义好相对的一个标准,是相对谁的相对值,如rem是相对根元素的font-size值,vw是相对视口的width等。
简单介绍涉及到的viewport和html的font-size值
device-width为浏览器的理想视口
在移动端,如果不设置viewport宽度为理想视口,viewport宽度通常为980px,这会导致文字很小,我们需要手动放大阅读。
rem是 font size of the root element,简单一点可以设置html的字体大小为固定值(一般默认为16px),则width直接使用5rem(换算为80px),也可以使用js根据viewport大小动态设置rem大小。
同时也要注意,我们经常会在js或者是一些media query的设置不同断电,总之如果CSS元素的位置或大小等影响布局的因素发生变化,这是可能会触发回流,进行重新布局和渲染,这是我们在开发过程中要尽量避免和减小性能损耗的。
根据background, border, box-shadow等样式和HTML内容,将Layout生成的区域填充为最终将显示在屏幕上的像素。
整个大概的流程就是这样,下面用个谷歌开发者的小实例把添加事件响应流程讲一下:
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critical Path: No Styletitle>
head>
<body>
<p>Hello <span>web performancespan> students!p>
<div><img src="awesome-photo.jpg">div>
body>
html>
主要就两个资源–HTML和jpg
蓝色条有两部分,分别代表HTML的下载(分为HTTP链接建立和HTTP传输资源两段)和DOM parse的时间
紫色竖线表示domContentLoaded事件触发
紫色条代表图片下载和图片渲染
红色竖线则代表onload事件触发。
仔细注意一下,DOMContentLoaded是发生在img请求之后一点,而综合前面的内容,我们知道DOM是增量构建的,DOMContentLoaded实际上是DOM树构建完成的时候,具体说是DOM解析完