浏览器底层渲染代码的全过程

261 阅读7分钟

浏览器拿到页面代码后,会开始渲染和解析代码,最后在页面中渲染出图形和效果

  • 渲染HTML和CSS代码:遵循W3C规则,GUI渲染线程去处理
  • 渲染JS代码:遵循ECMAScript(ECMA-262)规则,JS引擎线程去处理
  • 在渲染代码过程中如果遇到link/img/script[src=xxx]/audio/video等,浏览器需要开辟“HTTP”线程,从服务器端获取到对应的资源文件(文件中代码)

步骤

  • @1GUI在渲染HTML代码的时候,会分析出节点之间的嵌套关系,从而绘制出DOM树(DOM-Tree)
  • 渲染过程中遇到link,则开辟新的HTTP线程,去获取资源信息,GUI不受影响,继续向下渲染“异步”
  • 如果遇到的是style这个标签,则无需获取资源,代码本身就有,此时GUI直接去渲染即可“同步”
  • 如果遇到的是@import,也会开辟新的HTTP去获取资源,但是此时会阻碍GUI的渲染,只有当样式资源获取之后,GUI才会渲染新拿到的样式代码“同步”
  • 优化技巧
  • 如果CSS样式“代码比较少”,我们直接使用内嵌式即可(尤其是移动端,我们经常这么干);
  • 但是如果代码比较多,还用内嵌式,会导致HTML请求速度都很慢,此时我们使用外链式;除特殊必要,不建议使用导入式;
  • @2在渲染解析中,我们可能会开辟多个HTTP线程获取资源文件,同时GUI继续渲染直到DOM-Tree生成;在DOM-Tree生成后等待所有的CSS资源都获取到,然后按照最开始编写的顺序,GUI再去依渲染,最后生成CSSOM-Tree(CSS样式树)
  • @3等待CSSOM-Tree也生成后,会和DOM-Tree合并在一起,生成一个Render-Tree(渲染树):渲染树包含了页面绘制的具体规则(节点的位置、大小、样式等等信息都有了)=>Layout(布局排列),而我们所谓的重排(回流)就是重新生成Render-Tree的过程
  • @4根据Render-Tree进行分层以及构建详细的绘制方案 绘制图形的时候,是按照层,一层层的绘制的
  • @5交给显卡(GPU)开始进行绘制 => Painting(绘制/绘画),而我们所谓的重绘就是重新绘制 如果引发一次重排(重流),必然会进行重绘:但是单纯只重绘,不一定会重排(回流);
  • DOM的重排(回流)在第一次页面绘制完后,如果我们修改了页面中节点的“位置、大小、结构等样式”,浏览器需要重新计算绘制规则(也就是重新生成Render-Tree),这个过程就是重排;重新生成Render-Tree后需要重新的绘制;
  • DOM的重绘:如果节点的“位置,大小,结构等样式没有改变”,只是改变了一些基础的样式(例如:改变了文字或者背景的颜色),此时无需重新生成 Render-Tree,只需要重新Painting绘制即可

为啥说操作DOM消耗性能?

  • 因为操作DOM就有极大的几率会引发DOM的重绘/重排,所以性能消耗比较大!在所以,当代项目开发,我们已经告别了直接操作DOM的时代(JQ也就没有用了),而是基于MVVM(vue)和MVC(react)等数据驱动模式进行开发「我们只需要操作数据,框架会根据数据帮助我们渲染试图和操作DOM,而框架内部操作DOM的时候,做了大量的优化处理,来提高性能!」
  • 性能优化:减少DOM的重排(回流)
  • 放弃直接操作DOM,使用vue/react/angular等数据驱动框架
  • 读写分离:把修改样式和获取样式的操作分隔开,避免渲染队列的刷新
  • 元素的批量操作:文档碎片、模版字符串拼接,集中改变样式等
  • 模版字符串举例:
let str = ``;
for (let i = 1; i <= 5; i++) {
    str += `<span>
        ${i}
    </span>`;
}
document.body.innerHTML += str; 
//此操作只会引发一次重排(回流)
  • 文档碎片举例
  • Document.createDocumentFragment()
  • 语法
  • let fragment = document.createDocumentFragment();
  • DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
  • 因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
let frag = document.createDocumentFragment(); //创建一个文档碎片(临时的容器用来存储DOM对象的)
for (let i = 1; i <= 5; i++) {
    // 每一轮循环都把创建好的SPAN放在文档碎片中
    let span = document.createElement('span');
    span.innerHTML = i;
    frag.appendChild(span);
}
// 最后把文档碎片中的所有DOM元素,统一插入到页面中:引发“一次”重排
document.body.appendChild(frag); 
                      
                      
 此操作会引发“五次”重排:每一轮循环都会改变DOM结构
for (let i = 1; i <= 5; i++) {
    let span = document.createElement('span');
    span.innerHTML = i;
    document.body.appendChild(span);
}
  • 集中改变样式举例
let box = document.querySelector('#box');
// 集中改变样式
// box.style.cssText = 'width:100px;height:100px;background:pink;';
// box.className = 'box'
/* // 当前触发三次重排
box.style.width = '100px';
box.style.height = '100px';
console.log(box.offsetWidth); //刷新渲染队列
box.style.position = 'absolute';
box.style.left = '100px';
console.log(box.offsetLeft); //刷新渲染队列
box.style.top = '100px';
box.style.background = 'pink'; */

/* // 在新版的浏览器中,以下操作触发“一次”DOM重排:当代浏览器有“渲染队列机制”
box.style.width = '100px';
box.style.height = '100px';
box.style.position = 'absolute';
box.style.left = '100px';
box.style.top = '100px';
box.style.background = 'pink';

  • 开启GPU加速:
  • 修改transform样式,不会引发重排「不会对整体重排,只会对当前这一层进行重新渲染」:因为修改transform浏览器会新起一个文档流去渲染,并不会对原始层中元素位置、大小等信息产生改变
  • 我们平时修改样式,尽可能修改那些脱离文档流的{例如:position定位,我们修改top/left等信息}:虽然也会引发重排,但是渲染和计算的时候,也只是对当前这一层进行处理,其它层如果没有发生改变,则无需重新渲染
  • 基于JS实现动画「定时器触发、requestAnimationFrame」,我们一般会牺牲平滑度换取速度(性能),但是我们现在实现动画,基本上都是基于CSS3中的transform、animation实现,他们的性能会更好!!
  • 在GUI渲染DOM-Tree的时候,如果遇到img/audio/video/等,和link一样,都是单独分配HTTP线程去获取资源,不会阻碍GUI的渲染!但是真实项目中,第一次加载页面的时候,图片和音视频我们都会做懒加载:
  • 同源下,允许最多的HTTP并发数5~7个(也就是浏览器针对这个源,同时只能分配7~7个HTTP线程),所以如果把这些线程用来做图片资源的获取,其它资源都要排后获取了...而且图片本身获取就慢、如果获取还多,很有可能导致HTTP传输通道的堵塞,让其余正在获取的资源也获取慢了...
  • 虽然图片资源的获取不会阻碍GUI线程渲染,但是资源回来后,在最后页面渲染的时候,肯定是需要把图片渲染的,这样也延长了页面渲染的时间...
  • 如果遇到的是
  • 所以一般都把JS放在页面的底部「首先不想让其阻碍GUI渲染DOM树、而且放在顶部,此时还没有DOM树呢,我们无法获取DOM元素(只有不需要操作DOM的JS代码,放在顶部没啥问题);」
  • 问题一:虽然放在底部不影响DOM树的渲染,但是会影响CSSOM树和RENDER树等渲染「毕竟它是阻碍GUI处理的」
  • 解决方案:把其改为异步的操作,不要让它阻碍GUI的渲染
  • defet获取资源不会阻碍GUI渲染,但是需要等待GUI渲染完(生成Render-Tree完成)再去按照JS导入的先后顺序依次渲染JS代码「defer是考虑到了JS依赖顺序的」
  • async:获取资源的时候GUI继续渲染,但是资源一旦获取到,立即阻断GUI,继续渲染JS「哪个资源先回来,就把哪个资源先执行,没有考虑JS的依赖顺序」
  • 优化内容:真实项目中,我们最好都把JS放在底部导入,(并且多个JS合并成一个),最好在设置上defer/async
  • 把JS放在顶部导入,并且还能获取到DOM元素对象?(面试题)
  • 外链资源设置defer/async
  • 设置事件监听:load OR DOMContentLoaded
  • GRP:关键节点路径,分析底层处理机制,针对每个环节进行相关的优化(面试可加)