浏览器的底层渲染机制与优化

209 阅读7分钟

了解浏览器渲染页面要做什么事:

当我们从服务器获取代码之后,浏览器是如何把代码,渲染为页面及其相关效果的CRP(关进渲染路径)性能优化法则:了解浏览器底层处理的步骤,针对每一个步骤进行优化

JS中的同步异步编程

  • 同步编程:上一件事没有处理完,下一件事就无法处理
  • 异步编程:上一件事没有处理完,也能处理下一件事

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

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

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

  • GUI渲染线程:用来渲染和解析HTML/CSS的 以及绘制页面
  • JS引擎线程:用来渲染解析JS的 +HTTP网络线程:用来从服务器获取相关资源文件的(同源下,最多开辟5到7个,有的浏览器只有4个)
  • 定时器监听线程:监听定时器是否到时间(计时的)
  • 事件监听线程:监听是否触发事件 .....

底层渲染机制:

步骤一:生成DOM树(DOM TREE)

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

  • 遇到< link > :分配一个新的HTTP线程去获取对应的css资源,GUI继续向下渲染【异步】
  • 遇到< style>:无需获取资源,但是GUI也不会去立即渲染css代码,放置渲染顺序错乱;它会等到DOM解构渲染完成,访问的link等资源也获取到了,按照之前书写的顺序,一次渲染样式
  • 遇到@import:也需要服务器获取资源(基于HTTP线程),但是这个操作会把GUI线程 挂起,无法继续向下渲染,知道css资源获取到之后,GUI才会向下渲染【同步操作,阻碍GUI渲染】
  • 遇到< img>:和link一样的,也是异步操作,也会分配HTTP线程去获取图片,GUI继续向下渲染
  • 遇到 script 因为JS中要涉及DOM的操作,所以遇到script,会默认阻碍GUI的继续渲染;
    • 先分配HTTP线程去获取js的资源
    • 资源获取到之后在分配JS引擎线程把js代码渲染
    • 都渲染完之后GUI在继续向下渲染 .....

步骤二:生成CSSOM树(CSSOM TREE)

DOM树生成后,等待css资源都获取到,此时按照css书写的顺序,依次渲染和解析css代码(GUI线程),生成CSSOM树:以及选出每个节点具有的样式【含某些样式是继承过来的 样式都是自己写的】

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

把DOM树和cssOM树合并在一起生成渲染树

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

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

步骤五:分层

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

步骤六:Paining 绘制&重排

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

CRP 优化技巧:

  • 1.最好把css合并压缩为一个,只请一次九八样式获取到即可;分多次请求因为HTTP的并发限制和可能出现的网络拥堵等问题,导致并不如请求一次快

    • css合并为一个
    • js合并为一个
    • 雪碧图
    • ...
  • 2.进可能不要使用@import导入,因为它会阻碍GUI的渲染,如CSS样式代码不是很多,使用style内嵌式会更好(尤其是移动端开发);如果代码很多,还是使用link外链式好{最好把link放在head中};

  • 3.图片懒加载一定要处理:不要再第一次渲染页面的时候,让图片资源的请求去占用有限的HTTP线程以及宽带资源,优先本着CSS/JS资源获取;当页面渲染完之后,再去根据图片师傅出现再视口当中,来加载真实图片

  • 4.关于< script >的优化

    • 最好把< script >放在BODY的末尾,等待DOM结构加载完成,再去获取和解析JS[此时就可以获取页面中的DOM元素了]
    • 也可以基于事件监听去处理
      • window.onload:等待页面中所有资源(喊DOM结构/CSS/JS等资源)都加载完后触发
      • window.addEventListener("DOMContentLaded",function(){}):只需要等待DOM结构加载完就会触发,所以触发的时机比window.onload会早很多
      • 也可以给< script >设置 async 或者 defer异步属性
        • async:获取异步,渲染同步:遇到< script async >,会分配新的HTTP去获取资源,GUI继续渲染,当资源获取之后,立即结束GUI的渲染,让JS引擎线程去解析JS;JS代码渲染完才去执行GUI渲染。 特点:
        • defer 获取异步,渲染异步 ,会分配新的HTTP去获取资源,此时GUI继续渲染;当DOM结构渲染完成,而且设置defer的js资源也都获取到了,按照之前编写JS顺序,一次渲染解析JS代码。 特点:必须等待GUI及所有设置defer的JS代码都获取到了,再按照之前书写的顺序,依次渲染和解析,即实现了资源的同时异步获取,也可以保证JS代码之间的依赖关系!
  • 加快DOM TREE 的构造

    • 减少HTML 的层级嵌套
    • 使用符合w3c规范的语义化标签
    • ...
  • 加快CSSOM TREE的构造

    • 选择器层级嵌套不要过深
    • 减少CSS表达式的使用

DOM操作的性能消耗

操作DOM大部分性能都消耗在了DOM的重排和重绘上 页面第一次渲染必然会依次Layout(回流)和painting(重绘)第一次渲染完成后:

  • 重排(回流):如果浏览器的视口大小发生改变 或者页面中的元素位置发生改变 再或者DOM结构发生变化等,浏览器都要重新计算节点在视口中的最新位置,完成后再分层和重绘,此操作非常耗性能,因此要尽可能减少重排
  • 重绘:视口、元素的位置大小都不变,只是修改了一些基础样式(如,颜色,字体大小,透明度...)此时无需重新Layout,只需要重新painting即可! 回流必然重绘,重绘不一定回流

如果基于js操作DOM,那么前端性能优化比作的事:减少重排

  • 基于Vue/React/Angular 等框架进行开发,我们基于“数据驱动视图渲染”规避了直接操作DOM,我们只需要操作数据,框架内部帮助我们操作DOM
  • 读写分离: 新版本浏览器当中存在 “渲染队列机制”;当前上下文执行代码过程中遇到修改元素样式的操作,并不会立马执行,而是把其放到 渲染队列当中 代码继续向下执行 ,当代码执行完后,会把渲染队列中的修改样式操作统一执行依次,但是在此过程中遇到了获取元素样式的操作,则 刷新渲染队列 (就是把目前队列中的操作执行)引发一次重排
  • 批量新增元素
    • 基于模板字符串实现批量新增 ,字符串拼接会导致BODY原始结构中绑定的事件全部消失,所以此操作适用于:原始容器中没有任何内容,然后再把新的内容插入进去
    • 文档碎片:let xxx = document.createDocumentFragment() 创建文档碎片:装DOM的容器 xxx.appendChlid(xxx)创建元素先放置在文档碎片中document.body.appendChild(xxx) 最后统一把文档碎片中所有内容放置在body的末尾引发重排
  • 修改元素的样式尽可能使用transfrom变形属性( translate位移、scale缩放、rotate旋转)
    • 这个属性开启了硬件加速,不会引发重排
  • 如果必须引发重排,也把性能消耗降到最低
    • 尽量把修改样式的元素,单独放在同一个层面中(脱离文档流),这样即便重排,也只是对这一层的处理

    • 基于实现JS动画,尽量牺牲平滑度换取速度