关键渲染路径 CRF【摘抄】

@一棵菜菜  August 22, 2019

前言

一直想在前端性能优化方面有所提升,三年来在工作中总结出来的性能优化经验也很零散琐碎,而且有些知其然不知其所以然,越来越感觉系统性地学习这部分知识已经迫在眉睫。

了解到想要做好前端性能优化,首先是要知道浏览器渲染原理、关键渲染路径,如何利用工具来衡量性能,并运用简单的策略尽快向屏幕中渲染画面。要学习如何利用 Google Chrome 的开发者工具—— PageSpeed Insights 和时间表视图,找到所需的数据并立即提高性能。。。

谷歌官方的网络基础知识部分有非常详细的讲解,很赞!这些前端基础知识很容易遗忘或者混淆,所以在阅读过程中我摘抄整理成此文,也做了些自己的小结归纳。

路漫漫其修远兮!

本文摘抄自google文档《关键渲染路径》

线程

js 是单线程的。

GUI渲染线程

负责渲染浏览器界面,解析HTML构建DOM tree,解析CSS构建CSSOM tree;再组合构建Render tree,布局和绘制等。

当界面需要重绘(Repaint)或由于某种操作引发重排(relayout)时,该线程就会执行。

注意:GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

JS引擎线程

也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)

JS引擎线程负责解析Javascript脚本,运行代码。

JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序。

注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

一、什么是关键渲染路径 CRF ?

关键渲染路径:Critical Rendering Path,缩写CRF。

从收到 HTML、CSS 和 JavaScript 字节到对其进行必需的处理,从而将它们转变成渲染的像素 这一过程中有一些中间步骤,优化性能其实就是了解这些步骤中发生了什么 - 即关键渲染路径。

通过优化关键渲染路径,我们可以显著缩短首次渲染页面的时间。此外,了解关键渲染路径还可以为构建高性能交互式应用打下基础。

【更通俗的表述】关键渲染路径,也就是浏览器将 HTML、CSS 和 JavaScript 转换成实际运作的网站 而必须采取的一系列步骤。

事实上,当我们谈论关键渲染路径时,通常谈论的是 HTML、CSS 和 JavaScript。


二、关键渲染路径详解(浏览器渲染引擎工作原理)

当我们打开一个网页时,浏览器都会去请求对应的资源。服务器返回一个html文件给浏览器后,浏览器就会开始解析它。

浏览器可以从磁盘(浏览器缓存)或网络(http请求返回的资源)读取 HTML 的原始字节。

1. 将原始字节转成字符串

当浏览器接收到这些 原始字节 数据以后,将根据文件的 指定编码格式(如UTF-8)将它们转换成各个 字符串,也就是我们写的代码。

2. 令牌化

当数据转换为字符串以后,浏览器的 词法分析器(tokenizer)根据html规范将字符串转换为一个个令牌(token)

  • html规范包含了一组规则,规定了我们应该如何处理接收的数据。例如,在html中,尖括号里包含的文本具有特殊含义,表示的是令牌token。如<a>表示开始一个a标签。
  • 这一过程会将代码分拆成一块块,并给这些内容一一打上令牌,这个过程在词法分析中叫做令牌化(tokenization,或叫标记化)。

令牌化.png

3. 创建 Nodes

当词法分析器在执行令牌化的过程时,另一个流程正在消耗 (consume,处理) 这些令牌,并将他们转换成节点对象。

节点对象:node objects,每个标签的属性、值和上下文关系等都在这个文档对象里,即 nodes 包含了与html element (元素) 相关的所有信息

例如,我们消耗了第一个html令牌,并创建了HTML节点;然后消耗下一个head令牌并创建head节点。

创建node.png

4. DOM Tree

节点之间是有联系的,词法分析器发出了起始和结束令牌,表明了节点之间的关系。
Nodes根据令牌的层级关系(深度遍历)连接成DOM tree

比如StartTag:head令牌位于StartTag:htmlEndTag:html之间,表明head令牌是html的子节点,等等。如果StartTag和EndTag之间还有另一组StartTag和EndTag,那么表示一个节点中还有另一个节点。我们就是通过这种方式来定义DOM树的结构的。

形成DOM.png

最终,当消耗完所有令牌后,就形成了文档对象模型DOM。它是一个树结构,表示了html的内容和属性,以及各个节点间的所有联系。

DOM树的构建是增量式的(incremental。逐步发生的???)。

现在,我们就将下载的HTML转换成了文档对象模型 (DOM)啦,如下图。浏览器对页面进行的所有进一步处理都会用到它。

html转换成了DOM.png

注意:这些DOM对象包含了所有的属性。比如,html中图片节点包含来源属性src,对应DOM中的img节点也包含相同的属性。DOM代表了完整的HTML标记。

拓展:什么是DOM?

详见MDN《什么是 DOM》

【重点】DOM全称 domcument object model,即文档对象模型。DOM 以树结构表达 HTML 文档。

DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。不同的浏览器都有对DOM不同的实现。

【重点】DOM是web页面的 完全的面向对象表述
(或理解为:DOM代表了完整的HTML标记)。

JavaScript可以访问和操作存储在DOM中的内容。DOM 并不是一个编程语言。
window 对象表示浏览器中的内容,而 document 对象是文档本身的根节点。

DOM是一种树结构,之所以称之为树结构,是因为它每一层都像树的一部分,有主杆,枝干,以及细枝末节,一层一层往下发散。其中html是根元素(root element),body, head是html的子元素,而body 和head中又可以包含其它的子元素,如head中的meta , style , script等等,body中的div , h1 ,h2... p , form 等等,正是由于这样规范的层级构成了DOM的树结构。

HTML DOM 树形结构:

HTML dom 树形结构.png

下面是在web和XML页面脚本中使用DOM时,一些常用的API简要列表【熟悉牢记】

  • document.getElementById(id)
  • document.getElementsByTagName(name)
  • document.createElement(name)
  • parentNode.appendChild(node)
  • element.innerHTML
  • element.style.left
  • element.setAttribute()
  • element.getAttribute()
  • element.addEventListener()
  • window.content
  • window.onload
  • window.dump()
  • window.scrollTo()

例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" href="./index.css">
</head>
<body>
    <div class="header">
        <span class="page-name">文章详情页</span>
    </div>
    <div class="content">
        <h1 class="title">文章标题</h1>
        <div class="article">
            <p class="graph">吃葡萄不吐葡萄皮</p>
            <img src="./test.jpg" alt="文章插图">
            <p class="graph">不吃葡萄倒吐葡萄皮</p>
        </div>
    </div>
</body>
</html>

上面的html代码形成的DOM结构如下图:
dom tree 例子.png

5. CSSOM Tree

都是在GUI渲染线程内:在浏览器构建 DOM 树的同时,解析器如果发现了 css链接标签,将会新开一个线程去请求css资源,然后等待获取css字节内容(如果是内联样式就没有下载这一步)。
获取到css资源后,开始构建CSSOM树(字符串化 > 令牌化 > 创建Nodes > 形成CSSOM)。

可以在chrome的performance中看到将执行Recalculate Style事件,这个事件里将css转换为CSSOM树)。

CSSOM全称 Css Object Model,即css对象模型。CSSOM 以树结构表达样式规则。

DOM、CSSOM 区别

CSSOM 树和 DOM 树是独立的两个数据结构(都是独立的对象,但都是树结构),它们没有一一对应关系DOM 树描述的是 HTML 标签的层级关系,CSSOM 树描述的是选择器之间的层级关系

例子:

body{
    font-size: 16px;
}
// 去掉所有p元素的内外边距
p{
    margin: 0;
    padding: 0;
}
// 页面头部行高50px,文本垂直居中,隐藏
.header{
    height: 50px;
    line-height: 50px; 
    display: none;
    text-align: center;
}
.header .page-name{
    font-size: 20px;
}
// 文本区域左右两边留10px空白
.content{
    padding: 0 10px; 
}
.contetn .title{
    font-seize: 20px;
}
// 内容区行高30px
.content .graph{
    line-height: 30px; 
}
// 文章中的图片用作块级元素,水平居中
.content img{
    display: block;
    margin: 0 auto;
}

上面代码形成的CSSOM树类似下面的图:

cssom tree例子.png

这棵树是一个示意图,并不是浏览器里构造 CSSOM 树的真实的数据结构,而且各种浏览器内核实现 CSSOM 树的方式也不全都相同。它把 CSSOM 树描述成自上而下建立的结构。具体见《构建对象模型》
我个人觉得此时这张图里应该不包含html节点的。

6. Render Tree

DOM 树和 CSSOM 树都构建完成以后,浏览器才会将CSSOM 树和 DOM 树合并成渲染树(浏览器将阻塞渲染,直至 DOM 和 CSSOM 全都准备就绪):

从 DOM 树的根节点(html节点)开始遍历每个可见节点(不包含<head>节点和样式为display: none 等 不会体现在渲染输出中的节点),对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。发射可见节点,连同其内容和计算的样式。

最终输出的Render Tree 包含了屏幕上所有可见的DOM 内容及其 CSSOM 样式信息。

cssom 和 DOM.png

上图结合成了下图的Render Tree:

渲染树.png

7. 布局计算(Layout)

或者叫"自动重排"。

渲染树完成后,浏览器从 Render Tree 的根节点开始进行遍历,精确地捕获每个元素在视窗内的确切位置和尺寸(大小),所有相对测量值都转换为屏幕上的绝对像素。

布局可能会重新被以下操作触发:

  1. 手机上的设备方向更改;
  2. 浏览器窗口大小调节;
  3. 会修改 DOM 内容的任何其他操作。例如,向 DOM 树添加内容或从中移除内容、在节点上切换 CSSOM 属性等等!(引起重排、重绘)

优化布局的黄金法则:批量更新,避免出现多个布局事件

多次进行布局.png

此图中多次执行了布局计算。

meta标签中设置viewport

打造良好的移动设备体验,应该是始终记得设置viewport:

<meta name="viewport" content="width=device-width,initial-scale=1">
<meta>元标签,上面代码将告诉浏览器:布局视窗的宽度应该等于设备宽度。
如果没有这个meta标签,浏览器会使用默认的视窗宽度,通常是980px,它是针对大型屏幕优化的。

8. 绘制(Paint,渲染)

布局计算完成以后,浏览器将 Render Tree 中的每个节点转换成屏幕上的实际像素。

例子:向浏览器提供渲染树中的两个节点:

向浏览器提供渲染树中的两个节点.png

比如上图第二个div,绘制从 (0, 0) 到(100, 100)的长方形,将其区域渲染成白色。经过渲染引擎的处理后,div就显示在了屏幕上。
第二个比第一个绘制更快,因为第一个还需要呈现阴影、绘制背景图,第二个成本更低些。

网页性能的基本原则:先衡量,再优化(measure first then optimize)。


三、浏览器资源加载详解

在解析HTML的过程中,如果发现 HTML 里面包含外部的资源链接,如 CSS、JS 和图片等时(<link href=""> <script src=""> <img src=''>),浏览器会立即开启新的线程 并行下载 这些静态资源

JavaScript 会阻塞构建 DOM、延缓网页渲染;而html、css会阻塞渲染。css会阻塞其后js的执行

浏览器允许的并发请求资源数

浏览器允许的 同一个域名的 并发请求资源数 有限制。谷歌是6个。

针对性的优化方案:

1.减少网络请求数

(1)使用css spirit,将图标合成在一张图中,减少图片数量,减少http请求数;
(2)使用打包工具合并css和js,减少文件数量,减少http请求数。

2.增加静态资源来源

(1)将静态资源分布在不同的服务器中,使用多个域名,加大并发量;
(2)将静态资源和html文档分放在不用的域名下也有另一个原因,每次页面请求都会将相同域名下的cookie带给服务器端,实际上静态资源带上cookie是没必要的。

《理解浏览器允许的并发请求资源数》

1. CSS

概括:构建CSSOM 会阻塞渲染;也会阻塞其后js的执行。【牢记】

1. HTML 和 CSS 都是阻塞渲染的资源

因此需要将CSS尽早、尽快地下载到客户端,以便缩短首次渲染的时间!

2. 如何取消CSS阻塞渲染?【重点】

我们可以通过 CSS“媒体类型”和“媒体查询” 来取消CSS阻塞渲染。

通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制样式。声明您的css时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。
// 始终会阻塞渲染
<link href="style.css" rel="stylesheet">

// 它只在打印内容时适用,其他情况取消了阻塞渲染
<link href="print.css" rel="stylesheet" media="print">

// 屏幕大于900px时,浏览器将阻塞渲染,直至样式表下载并处理完毕;其他情况不阻塞
<link href="./maxScreen.css" rel="stylesheet" media="(min-width: 900px)" />

注意:浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。

3. 为何要在 CSS 构建生成 CSSOM 前阻塞渲染?

因为没有 CSS 的网页实际上无法使用。只有html的页面(没有任何样式)通常称为“内容样式短暂失效”(FOUC),只有html结构,很丑陋。所以浏览器将阻塞渲染,直至 DOM 和 CSSOM 全都准备就绪。

4. CSS 阻塞其后 JS 的执行【重点】

<head>
    <link href="style.css">
    <script src='script.js'></script>
</head>

注意:是"其后"的js,如果将js放在css上方(前方),则会直接执行js而不会被css阻塞。

阻塞流程.png

分析:浏览器请求HTML资源,一旦获得响应和资源就开始解析:构建DOM。
然后在解析过程中,发现css链接将立即开启新线程 并行 请求下载css资源;
然后解析器继续操作,并找到了script标签,此时script就开始阻塞构建DOM。浏览器不知道js将执行什么操作,因为js可能会尝试访问并修改css属性(或DOM),所以css会阻塞其后js的执行,直到获得css资源并构建完成CSSOM。然后浏览器才会执行js(来访问或修改CSSOM)。js执行结束后继续恢复构建DOM。

所以优化css很重要

推荐此图来源视频《有关 JavaScript 依赖的详细信息》

2. JS

JavaScript 是一种运行在浏览器中的动态语言,允许我们修改网页的方方面面。

默认情况下,JavaScript 会阻止 DOM、其后 CSSOM 的构建,也就延缓了首次渲染。
为了实现最佳性能,可以让您的 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。

我们的脚本在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 <script> 时,它必须暂停渲染引擎构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕(执行完脚本),浏览器会从中断的地方恢复 DOM 构建。
  • 除非将 JS 显式声明为异步,否则它会阻止构建 DOM。
  • 当浏览器遇到一个 script 标签时,DOM 构建将暂停,直至脚本完成执行。
  • JS 可以查询和修改 DOM 与 CSSOM。
  • JS 在文档中的位置很重要。
  • JS 前面的 CSSOM 构建完成后,才会执行此 JS。【重要】
  • 此JS 执行会阻止其后的 CSSOM 构建。【重要】

1. JS插入到html的方式

  • 内联 JavaScript 代码段
  • 通过 script 标签引入的脚本

相同点:

浏览器遇到<script>标签都会先阻止构建DOM,等待CSSOM构建完毕(css阻塞其后js的执行),再执行js脚本,js执行完毕后再恢复构建DOM。

不同点:

如果是通过外部引入 js 文件,浏览器必须停下来,等待从磁盘(缓存)或远程服务器 获取js资源,这就可能给关键渲染路径增加数十至数千毫秒的延迟。
【牢记】请求外链引入的的资源始终会花费一定的时间。css同样。
如下图:

js阻塞构建DOM.png

输出结果为: Awesome page with JavaScript is awesome
推荐此图来源视频《外部 JavaScript 依赖》

2.为什么默认情况下,所有 JS 都会阻止解析器构建DOM?

JS可以访问和修改DOM、CSSOM,由于浏览器不了解 JS 计划在页面上执行什么操作,它会作最坏的假设并阻止解析器。JS 里可能会出现修改已经完成的解析结果,有白白浪费资源的风险,所以 HTML 解析器干脆等 JS 折腾完了再干。

异步JS

实际上,脚本始终会阻止解析器,除非您编写额外代码来推迟它们的 执行

某些 如不修改DOM或CSSOM的js脚本,不应该阻止呈现。比如分析工具脚本。

1. 延迟执行JS

DOMContentLoaded

当初始的 HTML 文档被完全加载和解析完成之后,即 DOM 生成后,DOMContentLoaded 事件被触发,而无需等待页面加载完毕(即不必等到页面中其他 CSS 样式表、图片和子框架加载完毕)。此时就可以访问整个页面的所有dom元素了,可以通过id或者class等对DOM进行操作。

jQuery中的$(document).ready(function); 其实监听的就是 DOMContentLoaded 事件。并且可以同时编写多个,并且都可以得到执行。可以简写成$(function(){})

<link rel="stylesheet" href="style.css">
<script>
document.addEventListener('DOMContentLoaded',function(){
    console.log('3 seconds passed');
});
</script>
注意:DOMContentLoaded 事件必须等待其所属 script 之前的CSS样式表加载解析完成(即CSSOM构建完成后)才会触发。
如果将link置于script之后,就会立即打印。

onload

当页面所有资源(CSS样式表、图片、脚本等)都加载完成了( 不管资源是否加载成功;此时脚本也都执行完毕)浏览器就会发出onload事件,就会触发执行window.onload(),而非必须渲染结束后才触发!且 window.onload 只能执行一次。 我们可以等待该事件,然后再执行一些脚本。

参考此证明的例子《onload 属性到底是资源加载完成还是渲染完成测试》

官方描述:window.onload() 方法用于在网页加载完毕后立刻执行的操作,即当 HTML 文档加载完毕后,立刻执行某个方法。另外常用写法:window.onload = function(){};

jQuery中的$(document).load(function);监听的就是这个 load 事件。【待确认是load 还是 onload】

比较 DOMContentLoaded 和 onload

触发的先后顺序是:DOMContentLoaded -> load。(或者说 $(document).ready 的执行时间要早于window.onload。)
综上,建议使用DOMContentLoaded 或$(document).ready(),DOM加载完成后就可以执行,加快网速速度。
若需要等所有内容都加载之后操作,如图片的裁剪等,则用$(window).load()

2. 将外链 JS 标记为异步【重点】

优点:可以减少由于JS 的下载(请求)和执行 导致长时间阻塞GUI渲染引擎构建DOM、CSSOM,既可以让浏览器遇到<script>时继续构建 DOM,也能够让脚本在就绪后执行

实现方式:添加asyncdefer属性。(注意:必须是外链引入的js!)

<script async src="script1.js"></script>
<script defer src="script2.js"></script>

1.async:将在html解析期间并行下载 JS,JS 下载完成后 将立即执行这个JS并阻塞构建 HTML,执行完后再恢复解析HTML。【JS异步下载完后立即执行(执行时阻塞DOM构建)

前面的CSSOM不会阻塞此JS。即当JS下载完成后就会立即被执行。

其实这里是有两条线程的,一条JS引擎线程,一条GUI渲染线程(浏览器渲染引擎),但是他们两是互斥的,不能同时进行(执行),所以即使是async,JS执行时仍然阻塞DOM构建、CSSOM构建。

2.defer:将在html解析期间并行下载 JS,并在 HTML 解析完成后才会执行它(即 DOM 结构完全生成,以及其它脚本执行完成)。【JS异步下载,DOM构建完成后才执行

注意:
HTML5规范:"每一个defer属性的脚本都是在页面解析完毕之后,按照原本的顺序执行,同时会在document的DOMContentLoaded之前执行。" 但是在现实当中, 延迟脚本(defer)不一定会按照顺序执行,也不一定会在DOMContentLoaded事件触发前执行,因此最好只包含一个延迟脚本。

建议:

  1. 不操作DOM的脚本可以使用 async / defer。比如 Google Analytics 就是适合使用 async 的脚本。
  2. 操作 DOM 的脚本不要使用 async / defer!如果一定要使用,请把需要操作 DOM 的部分放在 DOMContentLoaded 事件回调中执行

    现在有很多框架的启动机制都是这样的,于是可以确保对于框架本身是可以异步加载的。比如 Angular 1.x 的手动 bootstrap 或 Angular 2.x 的缺省 bootstrap 机制都是这样。

思考:

平时react本身要先加载,然后react渲染到一个id上,怎么保证react操作时这个dom已经存在呢?或者说<script>写在html前面,怎么保证js执行时是能够操作到dom的?
参考答:延迟执行js,即使用监听DOMContentLoaded事件(jq中的$(document).ready(function);监听的就是此事件),或者使用window.onload()

流程图

异步js执行过程.png

练习:选择js加载执行的方式?阻塞、内联或异步?

选择js加载执行的方式.png

原题链接视频《选取javaScript方法》

更多内容查看《async和defer的区别》


四、优化关键渲染路径

“优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript 之间的依赖关系谱。

我们构建渲染树甚至绘制网页时无需等待页面上的每个资源:并非所有资源都对快速提供首次绘制具有关键作用。图像不会阻止页面的首次渲染,不过,我们当然也应该尽力确保系统尽快绘制图像!

在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。

DevTools 底部的onload 事件标记的点是 网页所需的所有资源均已下载并经过处理 的点,这是加载微调框可以在浏览器中停止微调的点(由瀑布中的红色垂直线标记)。

摘抄自谷歌的《关键渲染路径》-非常推荐

一般优化策略小结【重点,牢记】

a. Minify(减小),Compress(压缩),Cache(缓存):HTML、CSS、JS均适用;
b. 减小使用 阻塞渲染 的资源(CSS):使用内联样式;在标签上使用媒体查询以取消阻塞渲染;
c. 减小使用 阻塞解析 的资源(JS):使用内联js;延迟执行js;将外链 JS 标记为异步(在js标签中使用asyncdefer属性)。

注意:使用内联CSSJS,则无法实现相关代码在其他页面的重用了,反而增大了资源大小。所以应该根据实际情况来选择是否使用内联方式。

总结下来为:

1.减少 CRF 的资源字节数(大小)(根据第a点,Minimize Bytes)

即减小通过网络发送的数据量。数据量越小,浏览器就能越快获取数据,并开始处理数据和渲染页面。

2.减少 CRF 的资源数量(根据第b、c点,reduce number of critical resources)

(并非所有资源都很关键,尤其是css、js。比如使用媒体查询的css、使用异步的js(asnyc/defer)就不关键了)

3.缩短 CRF 的长度

减少关键资源数量、及其关键字节大小(文件大小?)

最佳情况下,我们需要在浏览器和服务器之间来回多少次才能渲染好页面呢?如果html文件很小,那么我们只需一个旅行路线就可以获取全部数据。所以最佳关键渲染路径长度为1。但如果网页文件很大,就需要来回多次来获取数据,将会很明显地影响网页渲染的速度。【不太理解,需要深入学习了解下】

例子1:
计算CRP指标.png

关键路径资源有2个:html、css。最佳情况下,需要请求2次资源,所以最少关键路径长度为2。

例子2:
计算 CRP 指标 2.png

【注意第三个方框值!】
关键路径资源有3个:html、css、js。
最佳情况下,需要请求2次资源。1次html;虽然会另外再发起2次http请求分别请求css、js,但是浏览器有最大资源请求数量策略,即同一域名下可以同时发起多个请求,所以此时请求css、js算同1批次的往返,所以最少关键路径长度为2。
如果每次往返(来回)都需要花费1秒时长,那么要在屏幕上呈现内容至少需要多长时间呢?1s*2次往返 = 2s。

例子3:

计算CRP指标3.png

根据 HTML 计算 CRP 指标。
关键资源有2个:html、css。图片资源不会影响页面渲染,此处js是异步加载的,不会阻塞html解析。所以关键长度也为2。

具体内容见视频《一般策略和 CRP 图表》


五、浏览器预加载器-预加载扫描程序【必须了解】

推荐文章《浏览器预加载程序如何加快页面加载速度》

Internet Explorer,WebKit和Mozilla都在2008年实现了预加载器,作为在 等待脚本下载和执行时 克服低网络利用率的一种方法。

工作流程:

当浏览器解析 HTML 被 JS 阻止时,第二个轻量级解析器会扫描标记的其余部分,寻找其他资源,例如样式表,脚本,图像等(css,js,img),这些资源也需要被检索(被取回,即被下载)。

然后预加载器开始在后台检索(取回)这些资源,目的是:当主HTML解析器到达它们时,它们可能已经被下载,因此减少了页面中后面的阻塞(时间)

(当然,如果资源已经在缓存中,那么浏览器将不需要下载它

预加载器从标记中提取出urls,但是不会、也不能执行javascript

我的理解:其实预加载器只是预下载资源,不会影响js执行,且js执行时仍然阻塞DOM构建。

例子:
该测试页有在head两个样式,然后在头部有两个脚本,然后在body内有两张图片,1个脚本,最后另一张图片。
(注意:下面的代码没有实际意义,仅例子结构为了方便思考)

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>hello</title>
    <link href="1.css" rel="stylesheet" />
    <link href="2.css" rel="stylesheet" />
    <script src="1.js"></script>
    <script src="2.js"></script>
  </head>
  <body>
    <img src="1.png" />
    <img src="2.png" />
    <script src="3.js"></script>
    <img src="3.png" />
  </body>
</html>

IE7中测试(未使用预加载器)

未使用预加载器.png

如果浏览器仍然像这样工作,那么加载页面的速度会慢,因为每次遇到脚本时,浏览器都需要等待脚本下载并执行才能发现更多资源。

IE8中测试(使用了预加载器)
预加载器.png

可见其他资源现在与脚本并行下载,为此测试用例提供了巨大的性能改进:7s vs 14s。

六、计算下载资源的时间公式

计算下载资源的时间例子.png

64kb转换成字节单位:65536b。 1个资源包大小为1460b,所以得到64kb需要45个包。

七、关键渲染路径流程小结【牢记】

流程小结.png

绘制CRP图标-难.png

  1. 首先,浏览器向服务器发起http请求,一旦获得响应和资源就开始解析HTML:构建DOM。

    (Begin constructing the DOM by parsing HTML)
    DOM可以逐步构建,并非一次性出现所有响应(结果),所以我们可能不会立即构建完毕。【疑问???】
  2. 解析过程中,解析器发现一个 CSS、JS 链接标签,将立即开启新线程 并行 请求下载资源。异步的 JS 在下载完成后将按规定操作(但始终不阻塞DOM构建。async:异步下载且下载完成后立即执行;defer:异步下载且当DOM结构绘制完毕后执行)。非异步的 JS 立即阻塞 DOM 构建。此时另一个轻量级的解析器将会在后台扫描余下标记,寻找其他可以开始下载的资源,找到后就会在后台下载它们【预加载器】。

    上图中,解析器发现jquery.js后将立即阻塞构建DOM。预加载器在head中发现了reddit-init.js,将会预下载此资源。

    注意执行js是同步的。JS将阻塞构建 DOM ;(css)构建 CSSOM 将阻塞阻塞渲染、阻塞其后 JS 的执行。(如果 JS 在CSS前引入,则此CSS构建CSSOM不会阻塞此JS的加载和执行,相反JS会阻塞HTML解析、阻塞其后所有CSSOM构建)

  3. CSS 会阻塞其后 JS 的执行。首先浏览器收到服务器返回的 CSS 资源后将构建 CSSOM ,当CSSOM构建完成后、并收到了JS资源后才会立即执行 JS。

    此处资源大小:css>js,所以浏览器虽然先收到了JS资源,但依然会等待CSS资源的下载和构建CSSOM.
  4. JS 执行完成后,浏览器将继续进行DOM构建。
  5. DOM 和 CSSOM 都构建完成后,浏览器将合并它们构建渲染树;
  6. 然后进行布局计算,获得渲染树中所有元素的绝对位置和尺寸(Layout);
  7. 再绘制(paint)网页。

可能出现的问题:【待整理】

  • JS放在head标签中,如果下载或执行时间很长,将会造成白屏(阻塞了DOM构建,无法生成渲染树完成后续渲染);
  • CSS放在页面底部,部分浏览器会造成FOUC现象(无样式内容闪烁),有些浏览器会造成白屏。
白屏和FOUC(无样式内容闪烁)的产生主要与浏览器的渲染机制有关,有的浏览器是等待html和css全部加载完成后再进行渲染(白屏问题),有的浏览器是先显示已加载的html内容,等到css加载完成后重新对内容添加样式(FOUC问题).

对于-webkit内核的浏览器(IE也会产生),在进行网页渲染时,会同时加载html和css分别构建DOM树和CSSOM,等两者都构建完成后,再绘制渲染树,然后将页面显示出来(DOM+CSSOM生成渲染树 ——>布局渲染显示完整的样式)。如果在html中将css文件放置在文档最后,那么将会导致CSSOM晚于DOM树的建立,浏览器需要等待CSSOM建立,然后然后才进行网页内容的绘制,这个等待的过程,没有内容显示,就导致了白屏的产生。因此在开发中,需要将CSS放在head标签内,让其与html内容同时被加载。

如果把样式放在文档底部,浏览器会等HTML和CSS完全加载完成之后再绘制到屏幕上去(DOM+CSSOM 生成渲染树>布局渲染显示完整的样式),譬如我们打开某些国外的网站可能出现加载时间过长,页面会出现白屏,而不是内容逐步展现。
主要代表有Firefox,如果把样式放在文档底部,会逐步加载无样式的内容,等CSS加载完成之后突然展现样式(FOCUS)。主要是由于浏览器先显示已加载的html内容,等到css加载完成后重新对内容添加样式导致的。

我的疑问:chrome明明是要DOM和CSSOM均构建完成后才会生成渲染树,那么就应该是出现白屏效果呀,为什么我测试把外链样式放在文档底部是出现FOUC问题呢?【???】
我的测试代码:

<body>
    <div class="note">hello every</div>
</body>
  <link
    rel="stylesheet"
    href="https://www.gstatic.cn/devrel-devsite/prod/v425077d6c7be97246d05a953898cb9591a173a3cef753a451b8729896196bc0a/developers/css/app.css"
    media="all"
  />      

所以:
通常将CSS文件放在head中(尽快下载css并完成CSSOM构建,加快生成渲染树);
将JS放在body后面(避免阻塞DOM构建)。

更多查看《关于白屏和FOUC现象》
更多查看《白屏问题和FOUC》
《关于无样式闪烁》

一般优化策略:

  1. 减少 CRF 的资源数量
  2. 减少 CRF 的资源字节数(大小)
  3. 缩短 CRF 的长度

下载HTML到渲染的流程:
下载到渲染流程.png

需整理笔记

返回部分html是个很好的性能优化策略。——逐步交付html太机智了
很遗憾,css无法使用逐步处理方式使页面显示更快。即无法使用部分cssom tree,因为可嫩改回导致我们在渲染网页时使用了错误的样式。
所以,浏览器此时会阻止页面渲染,直到收到并处理了所有的css.——css会阻塞渲染。

更加具体的标记实际上要求浏览器做更多的工作。它需要遍历DOM 树中的更多节点。

<div>
    <h1>aaa</h1>
    <p>bbb</p>
</div>

h1{font-size:12px}  // 这种写法样式渲染的更快。
div p {font-size:14px}

I had a feeling (that)you’d say that. 我就知道你会这么说


添加新评论