说说浏览器渲染

本文详细介绍了浏览器的组成部分,包括浏览器引擎、渲染引擎、网络、UI后端等。浏览器的渲染过程涉及DOM解析、CSS解析、合并CSSOM和DOM、布局和绘制。文章讨论了DOM操作对性能的影响,以及CSS放在开头、JS放在结尾的原因。还涵盖了回流和重绘的概念,以及首屏优化策略,如减少回流和重绘次数、利用requestAnimationFrame等。最后,强调了JavaScript的执行如何影响渲染,并告别了jQuery的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

浏览器

浏览器大体上由以下几个组件组成,各个浏览器可能有一点不同

浏览器组成

  • 界面控件 – 包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分
  • 浏览器引擎 – 查询与操作渲染引擎的接口
  • 渲染引擎 – 负责显示请求的内容。比如请求到HTML, 它会负责解析HTML、CSS并将结果显示到窗口中
  • 网络 – 用于网络请求, 如HTTP请求。它包括平台无关的接口和各平台独立的实现
  • UI后端 – 绘制基础元件,如组合框与窗口。它提供平台无关的接口,内部使用操作系统的相应实现
  • JS解释器 - 用于解析执行JavaScript代码
  • 数据存储持久层 - 浏览器需要把所有数据存到硬盘上,如cookies。新的HTML5规范规定了一个完整(虽然轻量级)的浏览器中的数据库 web database
  • 定时器触发线程:用来记数 --> node中 定时器也是通过时间循环来处理的 而不是单独的线程计数
  • 事件触发线程&HTTP请求线程:微任务?

线程与进程

每个窗口就是一个进程 会 new 一个引擎实例,进程之间不共享资源和地址空间
每个 Tab 就是一个线程,线程之间共享空间地址和资源,所以会有一些安全问题

浏览器常驻线程

  • GUI渲染线程:渲染界面(HTML),重回(repaint)、回流(reflow)的时候也会执行,在 js 脚本线程运行的时候,GUI 线程会挂起
  • js 脚本线程:用户交互逻辑、DOM CSS样式树操作
    • 为什么不用多线程:多个 js 线程操作一个 DOM,可能造成性能浪费/数据不一致,当然可以通过锁来控制,但是太复杂了 JS 最初选择了单线程
    • GUI 和 JS 线程为什么互斥:如果 js 在操作样式,GUI 也在渲染样式,可能会造成数据不一致问题
    • 互斥有什么问题:js 线程运行时间太长,GUI 被挂起到其他队列,会造成页面卡顿
  • 定时器触发线程
  • 事件触发线程
  • 异步http请求线程

传说中的DOM操作

    从去年正式接触到前端开发开始,就听很多人说直接操作dom成本高 性能差,尤其是在学习了React这类的MVx框架之后,前端也开启了data驱动view的模式,大学学习的jQuery还没怎么用就被毙掉了,那到底为什么DOM操作会严重降低前端性能呢?

DOM 文档对象模型,是Document Object Model的缩写,CSSOM css对象模型,是CSS Object Model的缩写

    在刚接触前端的时候,一直以为DOM就是div、span、Hx这些标签。DOM毕竟是model,html作为一种标记语言 在DOM模型中担任对象的角色,而DOM则为html标签提供‘编程API’,DOM不会去操作标签属性 内容等内部的东西。但实际开发中,光设计 不动态修改界面是不合理的,所以就出现了JS等脚本语言进行html级别的操作
    其实DOM操作不是JS的特权,貌似Python这些脚本语言也是可以的。而且在前端页面加载的时候,除了DOM 还有一个叫CSSOM的东西,负责解析CSS并形成树,和DOM是两套模型结构

说说浏览器渲染

    DOM操作成本,无非就是传输成本+渲染成本两部分。首先服务器和客户端(这里只说浏览器,其他的不太了解 不敢乱说 怕被打),挥个手啊 抱一抱啊巴拉巴拉的PY交易一波,就把文件从服务器搬到了浏览器,我们下面介绍的浏览器渲染 也只从这个时间点开始 只讲DOM渲染,JSP这种服务端渲染就不讲了。浏览器渲染主要步骤如下:

  • HTML解析,生成DOM模型树(如果这里有外链或者内联 会发出一些请求或者加载)
  • CSS解析,构建CSS样式树(同上)
  • 合并CSSOM和DOM
  • render布局
  • render绘制
  • 小尾巴

HTML解析

<html>
  <head>
    <meta name="viewport" content="initial-scale=1">
    <link href="xxx.css" rel="XXX">
    <title>HaHaHa</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students</p>
    <div><img src="XXX.png"></div>
  </body>
</html>
  • 我们以上面的代码为例,树的构建过程如下
  • 类似数据结构里面 树的先序遍历,只有当前节点的所有子节点都遍历 处理好了,才会去遍历兄弟节点
  • DOM 树的根节点就是 document 对象
    DOM CSSOM树的遍历

CSS解析

    css解析比html解析稍微复杂点儿,很多浏览器都有一套自己的样式库(又爱又恨的东西,有时候需要花很多精力去样式覆盖),所谓的css解析就是对样式库进行一次从通用样式(比如父级的 作用于某一块的‘全局样式’)到某节点具体样式的样式替换。最终输出的就是修改版的浏览器样式库

合并CSSOM和DOM

    合并CSSOM和DOM 生成render树,对于使用过React的同学,可以说是灰常亲切了。在浏览器渲染的流程中,render其实就是DOM和CSSOM的合体(怎么想起来了悟天克斯0.0)
render树

  • 在前端渲染,树的遍历都会经历 Bytes、characters 、tokens 、nodes、object model五步转变。首先从Bytes解析字符串,然后进行类似对象转变的过程 生成tokens,然后在一个一个的生成node节点,最终将node节点按从属关系遍历成树,完成遍历
  • 可以将 Render 树看成是 V,DOM 树与 CSSOM 树看成是 M,C 则是具体的调度者,比HTMLDocumentParser 等,Render树是用于显示,那不可见的元素当然不会在这棵树中出现了,譬如
  • 不过这里有个东西要提一下,就是React中的三目元算符(或者&&)display、visibility、opacity这四种情况,前两种在不显示的时候 是不会丢到模型树里面的(DOM树/CSSOM树/render树),后两种会放到树里面 所以会保留空间,之后在需要的时候进行绘制
  • DOM 树里面有 head、title、div什么的,render 树就比较加单,每一个节点都叫一个 renderer
    • 与 DOM 节点的关系是:1-1/n-1,比如 select 元素,就需要三个renderer:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮
    • renderer 与 DOM 元素的位置:可能不一样,因为如果设置了 float/position:absolute 脱离文档流, render 树会单独处理
      这里写图片描述

render布局(Layout布局)

    这一步又叫Layout布局,就是绘制图层的PX信息,比如基于当前浏览器的可视窗口大小,通过属性计算元素的相对尺寸、相对位置等

  • 左上角为 (0,0)基础坐标,从左到右,从上到下从DOM的根节点开始画,有子元素得先去确定子元素的显示信息
  • 布局阶段输出的结果称为box盒模型,精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位

render绘制

    客户端将图层数据传给GPU,进行图层绘制,然后显示在浏览器中

小尾巴

  • 以上渲染都是渐进式的,从网络上获取数据的期间会渲染一部分界面,增加用户体验

说说回流和重绘

    上文说到的回流和重绘,前者发生在布局阶段 后者发生在绘制阶段
    回流一般模型树发生了改变,比如 html 元素的内容、属性变化,就需要重新生成树、布局、绘制。而且一个元素的回流会导致子节点、兄弟节点甚至父点的回流,进而又是一大波重绘,所以性能消耗要比重绘大
    重绘一般是元素结构没有变化,只是颜色、背景等样式发生了变化,只需要使用新样式重新绘制界面即可。类似于截肢等身体改造要比换个外套要麻烦的多
    对于reflow和repaint,目前已知的优化是,部分浏览器对改变做了量化,只有改变到了一定的数量,才会执行相应的操作

  • reflow:比如发生页面初始化、窗口尺寸、元素的占位面积(字体修改)、定位方式、边距等属性的改变、padding 等会导致 render 树改变,以及 图片加载、外链、修改样式以及脚本操作 DOM/html 元素的 CRUD 等
    • 这里的R是特殊情况,R 即 read 浏览器在读取某些页面属性的时候会提前触发回流,来防止因为某些元素未回流 最终得到假数据,比如offsetXxx系列、scrollXxx系列,或者脚本使用了getComponentStyle 等实时获取元素属性的方法
    • 图片加载貌似只会影响布局+绘制
  • repaint:回流必然会引发重绘,改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性,只会单独引发重绘

为什么CSS放开头 JS放结尾

    这里的原因,挖到最底层 其实就是 css 和 dom 渲染的问题。在说这个之前,我们先嘴把嘴的脑补一个知识点,就是DOM事件,这里回顾两个:DOMContentLoaded + load事件

  • DOMContentLoaded:在得到DOM树之后触发,而不理会外链是否加载完毕,不过该事件支持在页面加载的早期添加事件处理程序,初衷目测就是希望可以和页面更早的交互吧。所以如果js脚本的加载是同步的,则会该钩子应该放在所有脚本最前面,否则如果发生阻塞,会延迟钩子的触发
  • load: onload这个方法只能触发一次,不过JQ的load可以触发多次。他会在页面的所有模型树、图片、外链加载完了才会触发。

    我们要想缩短渲染时间,除去布局和绘制这两步主要依赖计算机性能的环节,我们能做的应该是减少模型树的生成时间(当然 也吃机器性能)。所以这里又可以展开 css 阻塞渲染和 js 资源阻塞渲染

  • css阻塞渲染(包括JS):直到生成CSSOM树 才进行下一环节,所以 link/style 最好放在头部,以缩短渲染时间(sass的计算会不会发生阻塞呢?毕竟js单线程)
  • js阻塞渲染:因为js脚本是同步的,即加载完了就会执行,然后才会去继续下面的渲染。
    • 所以没有特殊需求,还是把js放在末尾比较好,也可以设置async和defer,后者相当于把js放在了最后面,前者会异步执行 但是不会保证顺序,虽然是在 load 之前的,但是如果脚本发生阻塞,还是会影响加载速度
      在
    • 它不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性,所以要阻塞其他环节
  • 上面的js加载完才会执行渲染:因为引擎线程和渲染线程是不同的东西,而且js还是单线程的,所以没法并发执行(脚本加载-引擎线程,脚本执行和页面渲染-渲染线程)。
  • 如果有css资源,在CSSOM未构建完之前,也会阻塞js进程
  • 如果页面有脚本,会直接触发js解析动作的执行,暂停并阻塞DOM渲染,等到CSSOM解析之后 才会执行js 然后渲染DIM
  • 执行顺序是请求页面—解析模型 如果碰到src/href 则加载外链请求—进行blocked操作 按照构建CSSOM(加载 css 文件)、执行js、构建DOM、render的顺序执行,但是只要某一环节断掉了就会从头blocked一次,比如在构建dom的时候 js外链加载好了 触发js解析执行,bocked操作就会暂停,然后重新从构建 CSSOM 开始执行一遍

引入顺序上, JavaScript 应尽量少影响 DOM 的构建

这些算首屏优化吗

  • css文件也要减少外链或者内联 – CSS 资源先于 JavaScript 资源 – 因为开头的 required 和 import会阻塞整个页面的渲染(从CSSOM这里开始阻塞)
    • 第一个资源会加载并阻塞。第二个资源设置了媒体类型,会加载但不会阻塞,print 声明只在打印网页时使用。第三个资源提供了媒体查询,会在符合条件时阻塞渲染
  • 尽量不要阻塞CSSOM和DOM的构建,比如异步/开头加载js脚本 导致DOM阻塞
  • 减少reflow 和 repaint的次数
    • 将样式修改集中,减少零散的修改(某些引发回流的读取也一样)
    • 使用DocumentFragment做缓存,然后将修改一次性的append
      • 或者clone一个节点做缓存,操作完之后 再替换原来的节点(参考
    • 可以设置display或者三目运算符等 在最终完善好元素之后 再显示元素
    • 降低float这类可以让元素脱离文档流的属性的使用(table布局会减少页面重排)
  • requestAnimationFrame 代替 setTimeout/setInterval
  • 长耗时的JS代码放到Web Workers中执行
    var dataSortWorker = new Worker("sort-worker.js");
    dataSortWorker.postMesssage(dataToSort);
    // 主线程不受Web Workers线程干扰
    dataSortWorker.addEventListener('message', function(evt) {
    var sortedData = e.data;
    // Web Workers线程执行结束
    // ...
    });
    
  • 优先使用渲染层合并属性、控制层数量,通过translateZ()、translate3d(0,0,0)、will-change 戳 4.5
  • 避免在输入事件处理函数里面修改样式,因为会累计到下次 requestAnimationFrame 的时候一起执行
  • 对滚动事件去抖

JQ爱妃 再见

    作为一个搬运工,这些知识当然都来自网络和书本,小弟只是看到、实践并整合了一波。有什么不对的 求喷。
    顺便痛苦的说一句:JQ爱妃 来生再见吧…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值