前言:中心思想还是让请求的资源得到更快响应的方法,比如压缩资源,减少数据量的大小,缓存数据以减少请求数量,http/2让网络传输变得更快这些,下面就让我们来看看浏览器是如何解析这些数据,最终又是如何将他们渲染在屏幕上的?在数据量不变的情况下还有哪些可以优化的点?
浏览器怎么渲染的,从输入一个路径地址开始
当浏览器获得一个html文件时,会“自上而下”加载,并在加载过程中进行解析渲染。
步骤: 构建DOM 和 CSSOM -> 构建render树 -> 布局render树 -> 绘制render树
1. 将HTML解析成一个DOM(文档对象模型)
浏览器会解析html文件中的标签,例如:<html>
,</body>
,<p>
,给这些标签打上标记,遇到标记后浏览器使用令牌生成器开始生成令牌,例如:<p>hello</p>
这样一个文本标签会生成:StartTag:p
, hello
, EndTag
:p这样3个令牌,接着生成对应的nodes(节点)后就形成了DOM,也就是文档对象模型(Document Object Model,简称DOM));
![在这里插入图片描述](https://img-blog.csdnimg.cn/fe932a8694dd40bc927fec0f0a07cd34.png
2. 将CSS解析成 CSSOM(层叠样式表对象模型)
当html解析中遇到< link >标签时,会请求对应的CSS文件,当CSS文件就位时便开始解析它(如果遇到行内< style
时则直接解析),这一解析过程可以和构建DOM同时进行。 那其实css树构建的过程和dom树类似。也是经历了打标记, 生成令牌和节点的过程,不过这里的识别标记规则和dom不一样,因为css拥有继承属性的操作方式,父级部分属性样式会被子级样式继承,比如字体font-size,
font-weight, font-style,
行高line-height等,还有部分默认样式,如下图箭头所指便是默认的样式,红色框内是继承样式。
3. 构造 Rendering Tree。
根据DOM树,会遍历每一个可见的节点,对于每一个可见的节点,在CSSOM上找到匹配的样式并应用,生成Rendering Tree。
此时渲染树并不等同于构建DOM 树,因为如果遇到不可见(display:none)属性,会跳过该元素和其子项。另外在html的head里不包含任何可见信息,所以这部分的内容会被很快的去除;
4. 布局
有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步就是计算出每个节点在屏幕中的位置和大小,这个操作称之为Layout。
5. 绘制
即遍历render树,并使用UI后端层绘制每个节点以像素显示在屏幕上。
这里需要注意的是:
1、 并不是所有的数据都已经加载出来了,而是先显示一部分,再显示另一部分。
2、 虽然是从上往下加载,但并不是一定要等着上面的内容显示之后才显示下面的内容,而是谁解析的快,就渲染的越快。
3、 CSS 加载不会阻塞 DOM 的加载,但是会阻塞 Dom 的渲染。虽然DOM 和 CSSOM 通常是并行构建的,但是Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成后,才能开始渲染。
如果引用的是外部样式,需要等待这些样式全部加载完后,才开始构建CSSOM。因为css文件中包含大量的样式,后面的样式会覆盖前面的样式,如果我们提前就构建CSSDOM,可能会得到错误的结果。
4、 JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建;在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
这是因为JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。
所以,如果我们想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer(延迟执行) 或者 async(异步下载) 属性;
关键渲染路径优化
关键渲染路径是浏览器将 HTML、CSS、JavaScript
转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是我们刚刚提到的的的浏览器渲染流程。
1、将能快速解析的数据放在前面,不好解析的放在后面
html语义化标签加强DOM解析,因为语义化标签是浏览器内置就能解析识别的标签;
尽量不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局,并且table不是一点一点加载渲染的,是一整个加载好了才渲染的;
一些装饰性元素可以适当的使用伪元素,避免增加无意义的页面元素;
2、让CSSOM不阻塞DOM的渲染
并不是所有的CSS资源都那么的 『关键』。
举个例子:一些响应式CSS只在屏幕宽度符合条件时才会生效,还有一些CSS只在打印页面时才生效。这些CSS在不符合条件时,是不会生效的,所以我们为什么要让浏览器等待我们并不需要的CSS资源呢?
我们可以将打印相关的CSS移动到print.css,然后我们在HTML中引入CSS时,添加媒体查询属性print,代码如下:
上面提供的方法是针对那些不需要生效的CSS资源,如果CSS资源需要在当前页面生效,只是不需要在首屏渲染时生效,那么为了更快的首屏渲染速度,我们可以将这些CSS也设置成非关键资源。只是我们需要一些比较hack的方式来实现这个需求:
<link href="style.css" rel="stylesheet" media="print" onload="this.media='all'">
上面代码先把媒体查询属性设置成print,将这个资源设置成非阻塞的资源。然后等这个资源加载完毕后再将媒体查询属性设置成all让它立即对当前页面生效。
类似的方案还有,代码如下:
<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">
<link rel="alternate stylesheet" href="style.css" onload="this.rel='stylesheet'">
总结一下就是把首屏渲染需要使用的CSS通过style标签内嵌到head标签中,其余CSS资源使用异步的方式非阻塞加载。优化过后可以对照看一下FP(首次绘制)的时间点有没有提前。
3、尽可能的不使用 @import 属性,import属性会在页面加载完成之后,等效于把css写到文档的底部,并且加载多个css文件时是串行加载,所以应该避免这种情况。
4、CSS 选择符从右往左匹配查找,应避免节点层级过多,并适当使用子选择器,例如 .list>a>img
5、异步JavaScript
为了避免阻塞,可以为script标签添加async或者defer属性。async的执行是加载完成就会立马去执行,而不像defer那样要等待所有的脚本加载完后按照顺序执行,下面可以听过这个两张图来对比一下他们的区别:
蓝色:文档解析
紫色:脚本加载
黄色:脚本执行
绿色: HTML 文档被完全加载和解析完成
defer
适用于如果你的脚本代码依赖于页面中的DOM元素(文档是否解析完毕),或者被其他脚本文件依赖时。
async
适用于如果你的脚本并不关心页面中的DOM元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据时。
6、减少JS操作 DOM 的次数
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》
DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”。
过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。
1、最常见的缓存Dom对象
操作Dom一般首先会去访问Dom,尤其是像循环遍历这种事件复杂度可能会比较高的操作,那么可以在循环之前就将主节点,不必循环的Dom节点先获取到,那么在循环里就可以直接引用,而不必去重新查询。
2、vue虚拟节点
虚拟Dom是js模似DOM树并对DOM树操作的一种技术。virtual DOM是一个纯js对象(字符串对象),所以对他操作会高效。
在dom发生变化的时候对虚拟dom进行操作,通过dom diff算法将虚拟dom和原虚拟dom的结构做对比,最终批量的去修该真实的dom结构,尽可能的避免了频繁修改dom而导致的频繁的重排和重绘。
减少 reflow/repaint
Reflow(重排):当浏览器发现某个部分发生了变化,影响了布局,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化——这就是Reflow,或是Layout。
Repaint(重绘):如果只是改变了某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘,重画某一部分。
因此看出,重排要比重绘更花费时间,也就更影响性能。所以在写代码的时候,要尽量避免过多的重排。
以下操作会导致重排或重绘:
-
删除,增加,或者修改DOM元素节点。
-
改变元素的大小,位置时,或者将使用display:none时,会造成重排;修改CSS颜色或者visibility:hidden等等,会造成重绘。
-
修改网页的默认字体时。 Resize窗口的时候(移动端没有这个问题),或是滚动的时候。 内容的改变,(用户在输入框中写入内容也会)。
-
激活伪类,如:hover。
1、减少DOM操作,比如改变元素的尺寸,位置,显隐等。可以预先定义好 css 的 class,然后修改 DOM 的 className;
2、为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
3、隐藏元素,进行修改后,然后再显示该元素。
4、可以使用文档片段创建一个子树,然后再拷贝到文档中,文档片段是一个轻量级的document对象,它设计的目的就是用于更新,移动节点之类的任务,而且文档片段还有一个好处就是,当向一个节点添加文档片段时,添加的是文档片段的子节点群,自身不会被添加进去。
let fragment = document.createDocumentFragment();
appendNode(fragment, data);
ul.appendChild(fragment);
懒加载(延迟加载)
1、图片懒加载
对页面加载速度影响最大的就是图片,一张普通的图片可以达到几M的大小,而代码也许就只有几十KB。当页面图片很多时,页面的加载速度缓慢,几S钟内页面没有加载完成,用户会失去耐心。
为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。参考库:vue-lazyload
<img v-lazy="/static/img/1.png">
2、路由懒加载
把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样做可以减少无效资源的加载,明显减少服务器的压力和流量,也能够减小浏览器的负担。
实现:结合 Vue 的异步组件和 Webpack 的代码分割功能
const Foo = () => import('./Foo.vue')
router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ]})
(图片 素材都来源网络,侵权联系立马删)