第二十五章 浏览器底层渲染机制

119 阅读9分钟

一、浏览器底层渲染机制

1、浏览器底层渲染机制:当我们从服务器获取代码后,浏览器是如何把代码渲染为页面及相关效果的

2、CRP(关键路径渲染)性能优化法则:了解浏览器底层处理的具体步骤,针对每一个步骤进行优化

3、js中的同步和异步编程:

  • 同步编程:上一件事没有处理完,下一件事无法进行处理;
  • 异步编程:上一件事即便没有处理完,也无需等待,可以继续处理后边的事情;

4、进程和线程:一个进程中可能包含多个线程

  • 进程:一般代表一个程序(或者浏览器打开一个页面就开辟一个进程)
  • 线程:程序中具体干事的人

浏览器是多线程的,当基于浏览器打开一个页面(开辟一个进程),会有不同的线程去做多件事情:

1)GUI渲染线程:用来渲染和解析HTML/CSS以及绘制画面;

2)JS引擎线程:用来渲染和解析JS;

3)HTTP网络线程:用来从服务器获取相关资源文件【同源下最多同时开辟5~7个HTTP线程】;

4)定时器监听线程:监听定时器是否到时间(计时作用);

5)事件监听线程:监听事件是否触发;

6)...

1、浏览器渲染底层机制过程

步骤一:生成DOM树 【DOM TREE】

当我们从服务器获取HTML代码后,浏览器会分配“GUI渲染线程”自上而下解析代码

  • 遇到: 分配一个新的HTTP线程获取对应的CSS资源,GUI继续向下渲染(异步);
  • 遇到: 无需获取资源,但是GUI也不会立即渲染CSS代码,防止渲染顺序错乱,会等待DOM结构渲染完成,访问的link等资源也获取到了,按照之前书写的顺序,依次渲染样式!
  • 遇到@import : 也需要从服务器获取资源(基于HTTP线程),但是这个操作会把“GUI线程”挂起,无法继续向下渲染,直到CSS资源获取到之后,GUI才会继续向下渲染(同步,会阻碍GUI渲染);
  • 遇到:跟是一样的,也是异步操作,会分配新的HTTP线程去获取图片资源,GUI继续向下渲染;
  • 遇到
  • 资源获取后再分配JS引擎线程把JS代码渲染
  • 都渲染完成后GUI再继续向下渲染
  • ...
  • 自上而下处理完后,目前只是把页面中的DOM结构(节点),构建出对应的层级!这就是DOM树!【触发DOMContentLoaded事件】

    步骤二:生成CSSOM树 【CSSOM TREE】

    DOM树生成后,等待CSS资源都获取到,此时按照CSS书写顺序,依次渲染和解析CSS代码(GUI渲染进程),生成CSSOM树:计算出每个节点具备的样式(包含继承样式和自己添加样式)。

    步骤三:合成渲染树 【RENDER TREE】

    把DOM树和CSSOM树合并在一起,生成渲染树!

    步骤四:Layout布局 & 回流/重排

    按照当前可视窗口大小,计算每一个节点在视图的位置和大小

    步骤五:分层

    计算每一层(每一个文档流)中各个节点的具体绘制规则

    步骤六:Painting绘制 & 重绘

    按照计算好的规则,一层层的进行绘制

    2、CRP优化技巧

    1、我们最好把所有的CSS合并压缩成为一个,只请求一次就可以把所有样式获取到;(分多次请求,因为HTTP的并发限制和可能出现的网络拥堵等问题,导致并不如请求一个快!)

    • caa合并为一个
    • js合并为一个
    • 雪碧图
    • ...

    2、尽可能不要使用@import导入式,因为他会阻碍GUI的渲染;如果CSS样式代码不是很多,使用style内嵌式更好【尤其是移动端开发(网络问题,HTTP可能获取太慢)】,但是如果代码很多,还是使用link外链式(但是最好把link放在中);

    3、图片懒加载一定要处理:在第一次渲染页面的时候不要让图片资源的请求去占用HTTP线程以及宽带资源,本着偶遇先CSS/JS资源获取;当页面渲染完成后,在去根据图片是否出现在视口中,来加载真实的图片。

    4、关于

    • 最好把
    • 也可以基于事件监听去处理
      • window.onload:等待页面中所有的资源(包含DOM结构/CSS/JS等资源)都加载完触发
      • window.addEventListener("DOMContentLoaded",function(){}):只需要等待DOM结构加载完成就会触发,所以触发的时机比window.onload会早很多!!
    • 也可以给
      • async (异步获取,同步渲染):
        • 遇到
        • 分配新的HTTP获取资源,GUI会继续渲染;
        • 当获取资源后,立即结束GUI渲染,让JS引擎线程去去渲染解析JS;
        • JS代码渲染完,再去执行GUI渲染!
      • defer(异步获取,异步渲染):
        • 遇到
        • 当DOM结构渲染完成,而且设置defer的JS资源也都获取到了,按照之前编写的JS顺序,依次渲染解析JS码
      • async 的特点是:只要是JS代码获取到,就会立即执行,不管书写先后顺序,适用于JS之间不存在依赖的时候"谁先请求回来先执行谁";
      • defer的特点是:必须等待GUI以及所有设置defer的JS代码都获取到,再按照之前的书写顺序,依次进行渲染和解析,即实现了资源的异步获取,也可以保证JS代码之间的依赖关系!

    5、加快DOM树的构建

    • 减少HTML的层级嵌套
    • 使用符合W3C规范的语义化标签
    • ...‘-

    6、加快CSSOM树的构建

    • 选择器层级嵌套不要过深(或者前缀不要过长)【选择器的渲染顺序:从右到左】
    • 减少CSS表达式的使用
    • ...

    3、回流(重排)& 重绘

    操作DOM比较消耗性能:大部分性能都消耗在了关于“DOM的回流(Reflow 重排)和重绘(Repaint)”

    页面第一次渲染,必然出现一次Layout(回流)和Painting(重绘);第一次渲染完成后:

    1)重排(回流):如果浏览器的视口大小发生变化或者页面中元素的位置、大小发生改变再或者DOM结构发生变化(删除、新增元素或挪动位置)等..., 浏览器都需要重新计算节点在视口中(本层)的最新位置(也就是重新Layout),完成后在分层和重新绘制!

    注意:此操作非常消耗性能,所以我们应该尽可能减少回流(重排)的次数!

    2)重绘:元素/视口的位置大小都不变,只是修改一些基础样式(例如:背景颜色、文字颜色、透明度...) ,此时我们无需重新Layout,只是需要重新Painting即可!

    注意: 重绘操作是必不可免的,只是想让页面第一次渲染完后还可以在改变,必须重新重绘;而且触发回流必然会引起重绘!!

    4、基于JS操作DOM的优化:减少重排优化

    如果基于JS操作DOM,那么前端性能优化“必做”的事情:减少DOM的重排(回流)

    (1)方案一:

    基于Vue/React/Angular等框架进行开发,我们是基于“数据驱动数据渲染”,规避了直接操作DOM,我们只需要操作数据,框架内部帮我们操作DOM(他们做了很多减少DOM重排的操作)!

    (2)方案二:读写分离

    新版本浏览器中存在“渲染队列机制”:当前上下文代码执行过程中,遇到修改元素样式大的操作,并不会立即修改样式,而是把其挪至到渲染队列当中;代码继续向下执行... ;当代码执行完成后,会把渲染队列中所有修改样式的操作统一执行一次(只触发一次重排);但是在此过程中如果遇到获取元素样式的操作,则“刷新渲染队列”(也就是把目前队列中的操作执行一次),引发一次重排!然后代码继续执行... ;直至代码全部执行;

    这样操作的好处是:

    • 把获取样式操作和修改样式操作分离开
    • 样式批量设置
    • 重排次数减少
    box.style.cssTest = "width:10px;height:20px;"
    box.className = "active"  --->active = {width:10px;height:20px}   //加属性
    

    (3)方案三:批量新增元素

    1)基于模板字符串实现批量新增

    let str = ``;
    for(let i=0;i<10;i++){
       str += `<div>${i}</div>`
    };
    document.body.innerHTML += str; 
    

    注意: 直接在innerHTML后边+=会导致BODY原始结构中绑定的事件全部消失,所以此操作适用于原始容器中没有任何内容的场景中直接可以把新的内容插入进去。

    2)文档碎片

    let frg = document.createDocumentFragment();  //创建文档碎片:装DOM元素的容器
    for(let i=0;i<10;i++){
       let divBox = document.createElement('div');
       divBox.innerText = i;
       frg.appendChild(divBox); //每创建一次元素,先放置在文档碎片中
    }
    document.body.appendChild(frg);  //最后同意把文档碎片中所有内容放在body末尾,引发一次重排
    

    (4)方案四:

    修改元素的样式尽可能使用“transform”【translate位移、scale缩放、rotate旋转...】,因为这个属性开启了硬件加速,不会引发重排(回流)

    (5)方案五:(最坏打算)

    如果真的引发了重排,也把性能消耗降到最低

    • 尽量把修改样式的元素,单独放在一个层面中(脱离文档流),这样即便重排,也是对这一层的处理
    • 基于JS实现动画,尽量牺牲平滑度换取速度(达到中和的状态)。