阅读 52

!!!详解:浏览器渲染页面的底层机制

👩:种一棵树最好的时间是十年前,其次是现在!
🌝:2021-07-07 又是阳光明媚的一天啊~
🙈:by 我的小小惠
复制代码

首先我们来了解一下「CRP」这个概念👇

CRP:即critical rendering path缩写,中文译文:关键渲染路径,那具体的是什么呢?

在每一个渲染的环节,了解其底层运行机制,从而做相关的优化

那就不得不讲讲「线程 & 进程」啦~

线程&进程

浏览器打开一个页面就是开辟一个进程(程序),在这个页面中要干很多事情,所以需要分配多个线程去处理这些事情,一个线程同时只能干一件事。

 通俗版(说人话):
 假设有一个饭店,这个饭店就是一个进程,雇了很多服务员,那一个服务员就是一个线程,
 很多服务员就是多线程。那就可以同时干很多事儿啦~~
复制代码
  • 进程大:进程中包含一到多个线程
  • 浏览器是多线程的
    • GUI渲染线程:自上而下渲染页面的(gpu渲染ui)
    • JS引擎线程:渲染和执行JS代码的
    • 时间触发线程:时间绑定的时候,会有一个线程监听事件是否触发,一旦事件触发,这个线程帮助我们通知绑定的方法执行
    • 定时器触发线程:设置定时器后,分配一个线程去监听是否到达时间,当到时间后,通知对应的方法执行
    • 异步HTTP请求线程:分配一个线程从服务器端获取内容「css/js...」
    • WebWorker
    • ...
  • JS是单线程的:因为浏览器只分配了一个线程「JS引擎线程」去渲染js

当浏览器从服务器端获取到HTML页面(代码)后,会分配一个GUI渲染线程,自上而下去渲染解析代码。

从css角度出发

  • @1 如果遇到(link/img)等标签,分配一个新的线程(http请求线程)去获取资源文件,于此同时GUI会继续向下渲染,无需管资源是否回来, “不阻碍GUI渲染”

  • @2 如果遇到(style),GUI此时会继续渲染代码,把内嵌样式渲染解析了。「但是如果在这之前,基于link发送过css资源的请求,那么此时不渲染,需要等到css资源请求回来,按照先后顺序渲染

  • @3 如果遇到@import 'xxx.css' GUI停止渲染,分配一个新的HTTP线程去获取资源文件,必须等到资源文件回来,GUI才会继续渲染,把获取的CSS代码进行解析... “阻碍GUI渲染”

  • ===>优化技巧:
    • 项目中尽可能不要使用@import 「排除less/sass中的@import ,因为这些代码编译完的时候,@import 就没有了」;

    • 如果css代码较少,可以直接使用内嵌式即可,减少http请求次数,让代码渲染更及时,移动端经常这样做。页面第一次打开速度更快。但是css代码较多就使用外链式「link」,这样方便代码维护,也不至于请求html页面就要很长时间。

浏览器渲染页面四部曲

  • 第一步:生成“DOM TREE”
    • 渲染和解析DOM结构,规划好节点和节点之间的关系
    • “DOM TREE”生成后,会触发一个js事件:DOMContentLoaded

===>优化:避免过深的DOM层级嵌套

  • 第二步:等待所有css资源请求回来后,按照导入的顺序,依次渲染css样式,要保证css的渲染顺序和优先级等问题 -->生成一个“CSSOM TREE”

===>优化:我们把link放在HEAD标签中,在渲染DOM之前就发送资源请求,这样等待DOM TREE生成之前,CSS已经请求回来,此时直接渲染即可,让事情同时去做,可以提高页面第一次渲染的速度。

页面同时并发的HTTP请求数量是5-7个「限定同源」,而且过多请求会导致网络通道阻塞...等很多原因导致,多次发送http请求,不如只发送一次请求快,所以真是项目中,我们需要把所有的css资源合并为一个文件!!这样无需考虑多个资源的渲染顺序

  • 第三步:把生成的"DOM TREE"和"CSSOM TREE"合并在一起,共同创建为“RENDER TREE”(渲染树)

    • 渲染数中,包含了最后浏览器渲染的时候,每个节点应该具备的样式「包含:自己写的样式、继承父级样式、浏览器默认的样式」。 window.getComputedStyle("元素")
  • 第四步:浏览器开始按照RENDER TREE进行渲染

    • @1 Layout布局/排列:根据浏览器当前视口(viewport)大小,计算出每个节点在视口中的位置
    • @2 分层(文档流):脱离文档流,构建每一层文档流,并且规划好每一层如何绘制「绘制的步骤都计算好」
    • @3 Painting绘制:按照分析好的规则开始绘制页面,最后在浏览器的视口中呈现出我们的页面

遇到script资源请求时,会发生什么呢?

  • 默认情况下遇到script,都会阻碍GUI渲染

    • @1 发送HTTP请求,获取资源文件,此时GUI渲染停止
    • @2 资源获取到之后,交给JS引擎线程去渲染和解析JS
    • @3 JS解析完成,GUI继续
    • 如果给script设置 defer或者async 则不会阻碍GUI渲染
  • script:请求资源和执行JS都会阻碍GUI

  • script async:请求资源不会阻碍GUI「开辟新的HTTP线程去请求,GUI渲染继续」,当资源请求回来后,会立即渲染解析JS,此时中断GUI渲染,当JS执行完,GUI才会继续

  • script defer:请求资源不会阻碍GUI,而且不会管资源啥时候获取到,都要等到GUI渲染完,并且所有的JS资源(设置defer的)都请求回来,最后按照导入顺序依次执行js「和link特别像」

  • async和defer区别:async不会考虑JS依赖关系,谁先请求回来谁先执行,但是defer需要等待所有资源都回来,GUI也渲染完了,此时再去按照依赖的顺序去执行JS

script是同步,如果加了defer和async是异步

提取前端性能优化方案

  • @1 避免HTML层级结构嵌套太深,目的:加快DOM TREE的生成

  • @2 CSS选择器渲染顺序从右到左,所以CSS选择器避免前缀过长,目的:加快CSSOM TREE生成 (a{} VS .box a{})

  • @3 优先使用style内嵌样式 目的:减少HTTP请求次数,加快CSS渲染

  • @4 样式过多的情况下使用,但是要把CSS资源合并为一个CSS样式文件「webpack可以自动打包」 目的:减少HTTP请求并且link不会阻碍GUI的渲染

  • @5 把link放在页面头部,目的:创建DOM TREE的同时,去请求资源文件

  • @6 坚决不用@import 「排除sass/less」,因为@import 会阻碍GUI渲染

  • @7 我们一般把script放在页面的末尾,如果非要放在顶部,最好设置defer或者async 目的:防止其阻碍GUI的渲染

  • @8 真实项目中,也需要把所有的JS资源合并为一个JS文件,目的:减少HTTP请求

=====>所有的优化目的,都是让页面渲染出来的速度更快,白屏等待的时间更短

重排(回流)

  • 页面初次渲染,必然会经历一次Layout排列(重排),计算出每个节点在视口中的位置
  • 当”删除或新增DOM元素、改变DOM元素位置、元素尺寸发生变化、内容发生变化...“等行为出现的时候,浏览器需要重新计算每个几点在视口中的位置(重新计算布局信息),也就是Layout重新来一遍,这样的操作非常消耗页面渲染性能,这就是重排(回流)。

重绘

  • 页面初次渲染必然会经历一次重绘,也就是Painting绘制,绘制出页面
  • 当”修改了元素的某些样式,例如:文字颜色、背景颜色、背景图片等“,这些样式不会影响页面的布局结构,此时我们只需要Painting的操作重新来一遍,这就是”重绘“。

重排必然会引发重绘,因为它必须经历:Layout->分层->Painting这个完整阶段

  • 我们平时所说的操作DOM耗性能,大部分指的就是DOM重排所以减少DOM的重排,是前端性能优化一个重要的指标

  • 浏览器的渲染队列机制

    • 在当前上下文中,如果遇到修改DOM样式操作,不会立即去修改,而是先放在”渲染队列“中,然后看后面是否还存在修改样式的操作,如果有则继续放在队列中...等到当前上下文执行完毕,会把队列中所有修改样式的操作一次性执行处理,这样只会引发一次DOM重排
    • 如果遇到获取元素样式的代码,则直接”刷新渲染队列“「意思是:把现在队列中修改样式的操作,立即进行处理」

如何减少DOM重排呢?

  • 1、放弃直接操作DOM,使用VUE/REACT等框架,基于数据驱动,实现视图渲染「本质:框架本身把DOM操作进行了封装,在内部实现了对DOM的优化处理」
  • 2、读写分离:把修改样式和获取样式的代码分离开”集中修改或集中获取“
  • 3、批量新增或修改元素:文档碎片或者模板字符串拼接
  • 4、使用CSS3硬件加速(GPU加速)
    • 修改元素的”transform/opacity...“等样式,不会引发原始文档流中的DOM重排,修改元素的transform,会把元素单独脱离出一个文档流,后期浏览器只是重新计算这个文档流中的位置和布局,对原始的其他文档流不会有任何影响
    • 同样的道理,我们后期修改元素样式,尽可能修改那些脱离文档流的元素样式,这样后期重新计算布局信息的时候,也只是对这层文档流重新计算,总比全部重新计算好很多
//这样会引发10次重排
for (let i = 0; i < 10; i++) {
  let span = document.createElement("span");
  span.innerHTML = i;
  document.body.appendChild(span);
} 
复制代码
 //这样会引发1次重排「基于innerHTML设置字符串内容,很容易吧容器中之前的内容/事件干掉」
let str = ``;
for (let i = 0; i < 10; i++) {
  str += `<span>${i}</span>`;
}
document.body.innerHTML = str; 
复制代码
//基于文档碎片实现DOM的批量设置:文档碎片就是DOM节点的临时容器
let frag = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
  let span = document.createElement("span");
  span.innerHTML = i;
  frag.appendChild(span);
}
document.body.appendChild(frag); 
复制代码

`小惠不易,随手点赞哦😝

文章分类
前端
文章标签