Open fi3ework opened 6 years ago
thks,这些应该都是提升用户体验吧,该下载的资源还是要下载。
@zhangzhongjiang 是,该下载的资源是要下载的,关键路径指的是指开始下载到 html 渲染到屏幕这段过程,让这段过程尽可能的快,减小阻塞。
我想知道, html渲染是等html所有结构下载完之后,在渲染,还是下载一部分,渲染一部分。 A: -> 所有下载完成, 开始解析渲染
B:
是A 还是 B,或者两者都不是,求解答
non-critial.css 加 preload 是不是有问题啊 不是应该是 prefetch吗
@kukufengfeng non-critial.css是指非首屏渲染的css,但是当前页面的css
关键路径
浏览器加载流程
浏览器在渲染页面时需要将 HTML 标记转化成 DOM 对象
CSS 则会被转化成 CSSOM 对象
DOM 和 CSSOM 是独立的树形结构,
当 DOM 树和 CSSOM 树都构建完成的时候,他们就会合并在一起构建 render tree,因为要在页面上渲染不仅需要这个页面的结构,也需要知道整个页面的样式,所以 render tree 是 DOM 树和 CSSOM 树的结合体,有了 render tree,浏览器才能知道把什么内容按照什么样式渲染在屏幕上。
浏览器从获取 HTML 到最终在屏幕上显示内容需要完成以下步骤:
经过以上整个流程我们才能看见屏幕上出现渲染的内容,优化关键渲染路径就是指最大限度缩短执行上述第 1 步至第 5 步耗费的总时间,让用户最快的看到首次渲染的内容。
另外,这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来,因为 HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历。
阻塞渲染的因素
外部样式表
从上面的整个流程我们已经知道,浏览器的渲染需要 render tree, render tree 需要 CSSOM 树才行,所以样式表的加载是会阻塞页面的渲染的,如果有一个外部的样式表处于下载中,那么即使 HTML 已经下载完毕,也会等待外部样式表下载并解析完毕才会开始构建 render tree。
脚本
脚本就更麻烦了,先明确一点, JS 引擎和 UI 的渲染引擎是互斥的,所以当脚本在执行的时候浏览器要将控制权就给 JS 引擎,等到 JS 执行完毕再还给 UI 引擎,不论这个脚本是以何种形式加载的,在执行时均会阻塞 UI 的渲染。
接下来分别看不同形式加载的脚本对页面渲染的阻塞情况:
内联脚本
内联的脚本随着 HTML 一起下载,在开始执行时已经完成了
字节 → 字符 → 令牌 → 节点 → 对象模型
的整个过程,所以不存在下载的时间(其实也不能这么说,下载的时间算在了 HTML 的下载时间中),执行时是会阻塞关键渲染路径的。外部脚本
外部脚本的整个加载过程及执行过程都是阻塞关键渲染路径的。
带 defer 和 async 的外部脚本
带 defer/async 的脚本会与 HTML 并行下载,下载的过程不会阻塞 DOM 的构建,但是执行是会的,不同的是 defer 是在 DomContentLoaded 之前执行,async 是加载完之后立刻执行。
defer/async 的脚本在下载期间不会阻塞页面解析不是一个技术原因而是一个选择,因为内联脚本/外部脚本是要等待他们执行,所以不得不等待他们下载。而页面并不需要等待 defer/async 的脚本,所以他们的下载与页面的解析是并行的。
动态生成的脚本
动态生成的脚本的下载过程不会阻塞页面的解析,执行会阻塞解析,有点 async 的感觉。
脚本与样式表的依赖关系
脚本不仅能够访问 DOM 元素,还能访问 DOM 的样式,如果将要执行脚本时浏览器尚未完成 CSSOM 的下载及构建,浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。
所以,CSSOM 的构建会阻塞 HTML 的渲染,也会阻塞 JS 的执行,JS 的下载与执行(内联及外部样式表)也会阻塞 HTML 的渲染。
优化方法
为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:
优化关键渲染路径的常规步骤如下:
关键 CSS
上面已经分析过了,样式表会阻塞渲染,在加载完毕之前是不会显示的,为了让用户以最快的速度看到页面上的内容,可以将页面的某一部分的样式抽离出来,单独放在一个样式表中或者内联在页面中,这样的样式称为关键样式,这部分样式会优先它可以是页面的骨架屏或者是用户刚加载进页面时看到的首屏的内容。
预加载 —— preload & prefetch
使用 preload meta 来提升资源加载的优先级。preload 的定义
注意和 prefetch 的区别
preload 会提升资源的优先级因为它标明这个资源是本页肯定会用到 —— 本页优先
prefetch 会降低这个资源的优先级因为它标明这个资源是下一页可能用到的 —— 为下一页提前加载
preload 最大的作用就是将下载与执行分离,并且将下载的优先级提到了一个很高的地步,再由我们去控制资源执行的位置。
加速样式表下载
样式表是阻塞页面呈现的(注意是呈现,不是解析),正常通过 link 加载的外部样式表要等下载,构建 CSSOM 树才会让页面呈现完成,但是 preload 能够让样式表的下载和呈现分离。
试想,当你在页面的 head 中写了如下的两个样式表:
第一个是关键 CSS,第二个不是关键 CSS,当页面解析了这两个 link 标签后开始下载,但是即使 critical.css 下载解析完毕也不会呈现页面,因为页面还要下载和解析 non-critical.css。
这时候,就要将 non-critial.css 作为预加载,当样式表作为被 preload 后,他就不会再阻塞页面的呈现,也就是所谓的异步下载,修改后的代码如下:
如此一来,页面在解析完 critical.css 之后就会呈现(暂不考虑脚本),而 non-critial 也在下载,但是并不阻塞页面,指导它下载和解析完毕后才会应用到页面上。
现在并不是所有的浏览器都支持 preload,我们可以用 loadCSS 这个库来做 polyfill,其实现的思路也是遍历所有带 preload 和 as 的标签,然后修改标签的 media 为不匹配任何条件并开始下载,在下载完毕后再还原该 link 原来的 media 标签将它应用。
加速脚本下载
preload 将脚本的加载及执行分离,加了 preload 的
<link>
标签的作用是将脚本提到高优先级尽快完成下载,但并未执行。还需要在你想要他执行的地方引入一个正常的
<script>
标签执行这个脚本否则 chrome 大约会在 3s 后报一个 warning 来提醒你这个资源被浪费了完全没有被使用到。
preload 的功能听起来很像被 defer 的脚本,但是:
DOMContentLoaded
执行前触发DOMContentLoaded
事件根据脚本在文档中的位置不同和他们是否是 async,defer 和阻塞,它们会有不同的优先级:
我们以掘金的首页为例:
可以看到 high 的全是写在 HTML 中进行加载的静态资源,Low 的都是 thunk 在 JS 中的脚本,是为其他页面预加载的。
加速字体下载
自定义的字体在加载之前会处于 FOIT(Flash of Invisible Text)现象,具体的可以看 这篇文章,虽然我们可以使用类似 webFont 一类的库来控制字体的闪现和添加钩子函数,但最佳解决方法还是让字体的加载达到最快的速度。
使用 preload 也可以来加速字体的下载,在 head 中声明 preload,比先下载样式表再从中读到
@font-face
的 src 再去加载要快得多。但是要注意
preload 如果不带 crossorigin meta ,默认情况下 (即未指定 crossorigin 属性时), CORS 根本不会使用,这样 http 的 request header 中就不会有 origin,默认不去跨域,但是 @font-face 中去加载字体是默认跨域请求的,所以会造成两次的 request header 不同,无法命中缓存,造成重复请求。
解决方法就是带上 crossorigin,
空关键字和无效关键字都会被当做 anonymous。
其他资源
preload 不仅可以将这些在 head 中的资源加速,还可以提前加载一些隐藏在 CSS 和 JS 中的资源,比如刚才隐藏在 CSS 中的字体资源,或者 JS 中请求的资源。
preload 的标签可以动态生成,这意味着在任何时候你都可以在页面中提前加载但不执行一个脚本,然后通过动态脚本来立刻执行它。
媒体查询
现在的页面基本上都具有响应式设计,即针对移动端或桌面端会采用 media 进行媒体查询,有两种包含媒体查询的 CSS 代码的方法:1. 将需要媒体查询的代码和基础样式代码放在同一文件中,使用
@media
来使媒体查询生效。 2. 将需要媒体查询的代码放在单独的一个外部样式表中,使用media
meta 对需要媒体查询的 link 进行控制。这两种方法各有好处,如果需要媒体查询的代码量很小,那么和基础样式放在一起也没有关系,可以节省一次 HTTP 请求。如果比较大的话,那么就会让样式表的体积增加,造成 FOUC 的时间变长,这时候更适合使用第二种。
另外请注意“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论哪一种情况,浏览器仍会下载 CSS 资源,但是不阻塞渲染的资源优先级较低。
优先级较低意味着浏览器在解析 HTML 时发现要下载这个样式表,但并不一定会立刻开始下载,而是可能会将它滞后一段时间再下载(等级低没人权),从 DevTools 上也可以看到 Highest 和 Lowest 的区别。
如果媒体查询的样式表符合当前的页面,那么媒体查询的样式表也会阻塞关键路径渲染(就好像他是个正常的一样),同时,它的下载优先级也会恢复到最高(恢复人权)。
media 配合 preload 能做到响应式加载资源,如下代码,分别是两副图片适配移动端与 PC 端,如果不加 preload 的话,那么其中一幅就会以 Lowest 的等级延迟加载,但是如果我们是一个移动端优先的网站,不希望用户浪费流量及网速下载PC 端的大图的话,就在每个 link 上加上 preload 即可,只有在打开网页时符合 media 的资源会被加载,不符合 media 的资源始终不会被加载,即使后面将浏览器的宽度拉宽也不会加载。
如果用户真的拉宽了屏幕,或者切换端设备,可以使用
Window.matchMedia
,来进行 media 的匹配。DNS 预解析 —— dns-prefetch
dns-prefetch 的使用方法更加简单:
link 标签的 rel 设定为 dns-prefetch,href 设定为需要预加载的主机域名即可。
在讲 dns-prefetch 之前,先复习一遍 DNS 的作用及可以优化的点才能了解 dns-prefetch 带来的好处。
一图流表达如下,其中 3, 4, 5, 6, 7 都属于 DNS 解析的过程,也是 dns-prefetch 发挥作用的地方。
dns-prefetch 主要用来在用户点击一个链接之前解析对应的域名,这会自动去调用用户浏览器的解析机制。浏览器会在用户浏览网页时多线程完成预加载,当用户真正点击的时候就节省了用户等待域名解析的时间。
Chromium 的官方文档中很详细的介绍了 pre-fetch:
Chromium 会根据页面中超链接的 href 去寻找主机名自动去 prefetch
如果访问的链接被重定向,那么浏览器可能无法自动识别出真正的主机进行 prefetch,此时需要我么手工预加载,也就是使用 prefetch 标签来指定主机。(这也是决定是否使用 dns-prefetch 的判断方法)
预加载不会对页面渲染造成损害,因为 Chromium 有8个专门用来预加载的线程。
dns-prefetch 带来的网络消耗是很小的
但是用最小的网络开销代价可以换来较好的用户体验。
默认情况下,Chromium 和 Firefox 出于安全考虑会关闭在 https 下的自动预加载,可以通过指定 meta http-equiv 来开启自动预加载。
PS: 如果通过 meta 显示的关闭了预加载,之后将无法再次开启预加载。
拿知乎举个例子,打开知乎,进入控制台,搜索 dns-prefetch
发现知乎用了如下的 link,都是知乎的静态资源服务器,因为在没有缓存(假设没有打开过知乎)时打开某个知乎页面,如果该页面有图片,并且是从以上的域名获取的话 dns-prefetch 就不会起作用。如果没有图片,那么上面的 dns-prefetch 就会解析域名,等到打开一个有图的知乎页面时 DNS 解析已经完成了。
DNS 预解析 + TCP + TLS —— preconnect
提前加载整个页面 —— prerender
以上两者详见:
参考
Preload: What Is It Good For?
用 preload 预加载页面资源
[译]Preload,Prefetch 和它们在 Chrome 之中的优先级
Preload, Prefetch And Priorities in Chrome
CORS settings attributes
Understanding Critical CSS
通过rel="preload"进行内容预加载
DNS Prefetching
预加载系列一:DNS Prefetching 的正确使用姿势
浏览器的工作原理:新式网络浏览器幕后揭秘