前端性能指标和优化目标——布局(layout) 与绘制(paint)

常用的性能测量API

利用性能测量工具可以去模拟用户的使用场景进行性能分析和评估。这些有时候还不够,这就需要通过动态的测量,借助web提供的标准的api。

web标准API:
  • 关键时间节点(Navigation Timing , Resource Timing
  • 网络状态(Network API
  • 客户端服务端协商(HTTP Client Hints)& 网页显示状态(UI API)

浏览器渲染原理,关键渲染路径(critical rendering path)

无论是首次加载还是后面页面发生样式上的变化,都会经历下面其中的几个步骤,最终将页面呈现给用户。

1.Javascript:可以通过JavaScript实现页面中视觉上的变化,比如增删DOM元素,也可以用css做动画,也可以用 webAnimation API,都可以触发视觉上的变化,其实这里叫JavaScript并不准确,这里准确的讲是触发视觉上的变化;

2.Style:对样式进行计算,这里会根据选择器对样式重新进行匹配,计算哪些元素的样式发生了变化;

3.Layout:布局,把元素按照计算后的样式绘制到页面上,这其实是一个几何问题,需要知道元素的大小和位置;

4.Paint:绘制,这里是真正的将元素画到页面上,绘制会和最后的composite(复合,合成)的过程联系起来,浏览器为了提高效率,并不是把所有的东西直接都画在同一个层里(可以类比Photoshop),最终会将这些图层合成在一起显示给用户;
5.Composite:复合,合成。
在这里插入图片描述
理论上这五个步骤是必须经历的,但是有些样式不会影响到我们的布局,也不会影响到我们的绘制,所以浏览器就进行了优化来加速渲染,不经历布局和绘制过程的渲染效率会更高。

当浏览器拿回服务端返回的资源后,读取的html、css和JavaScript都是代码,也是文本,计算机理解不了文本,浏览器会通过解释器将文本转换成它能理解的数据结构。
html部分:浏览器下载完html文档后会读取里面的代码文本,先把文本转换成单个的字符,解释器会一个字符一个字符地读取。html里面有很多标签,标签是通过一对尖括号标记出来的,这个尖括号就会被用作识别,那一堆字符串就会被理解成有含义的标记,这些标记最终被换成节点对象放到一个链形的数据结构里,这个链形的数据结构就类似下面的一颗树:
前端性能指标和优化目标——布局(layout) 与绘制(paint)_第1张图片

这颗树可以很好地将html文本的嵌套关系表达出来,通过这颗树就可以把html的内容、属性以及节点之间的所有关系表达出来,这个也是dom(document object model文档对象模型)。

css部分:当解释器遇到css样式文本后将里面的标记识别出来,将样式与节点一一对应起来,同样用树形结构将关系存储起来:
前端性能指标和优化目标——布局(layout) 与绘制(paint)_第2张图片

浏览器构建对象模型:俗称构建两颗树

  1. 构建DOM对象
  2. 构建CSSOM对象

接着两颗树进行合并:
前端性能指标和优化目标——布局(layout) 与绘制(paint)_第3张图片

布局(layout) 与绘制(paint)

布局(layout) 与绘制(paint)是关键渲染路径中最重要的两个步骤也是开销最大的两个步骤。
渲染树只包含网页需要的节点,布局会根据渲染树计算每个节点精确的位置和大小,布局关注的是元素的几何信息,绘制是像素化每个节点的过程将元素画到页面中,在页面首次加载中关键路径会都经历一次。在用户后续交互中有些操作可以不进行布局和绘制,比如有些样式的变化不会触发布局和绘制。布局关心的是位置和大小,比如元素的宽高(width,height)和offset,像这些几何信息发生变化的时候才会触发布局,像元素的颜色,阴影发生变化的时候不会触发布局,只会触发绘制,在关键布局中layout就会跳过直接进行paint;像有些动画可以通过Gpu进行加速,这种动画实际上可以直接走复合的过程而不需要进行布局和重绘。总结:影响回流(reflow)(再次布局)的操作。

  • 添加/删除元素
  • 操作style
  • display:none
  • offsetLeft、scrollTop、clientWidth
  • 移动元素位置
  • 修改浏览器大小,字体大小

这里做个测试,在页面onload后改变第一个卡片的宽度以及onload后不做任何操作做个对比,相关代码如下:

//获取所有卡片
        let cards = document.getElementsByClassName('MuiCardMedia-root');

        const update = () => {
            //改变第一个卡片的宽度
            cards[0].style.width = '800px';
        }
        // load事件后触发
        window.addEventListener('load', update)


关键的时间节点在performance—>Timings中都有标记,这里主要关注标红的L标记处,也就是onLoad事件之后,下面有一些task,这是主线程里面做的一些操作,load事件之后恰好触发了layout,这是修改了第一个图片的宽度而触发的。
前端性能指标和优化目标——布局(layout) 与绘制(paint)_第4张图片
当批量修改图片的宽度时相关代码链接

  //获取所有卡片
        let cards = document.getElementsByClassName('MuiCardMedia-root');

        //修改所有卡片宽度的方法
        const update = (timestamp) => {
            console.log(timestamp, 'timestamptimestamp');
            for (let i = 0; i < cards.length; i++) {
                cards[i].style.width = ((Math.sin(cards[i].offsetTop + timestamp / 1000) + 1) * 500) + 'px';
            }
            window.requestAnimationFrame(update);
        }
        // load事件后触发
        window.addEventListener('load', update)

前端性能指标和优化目标——布局(layout) 与绘制(paint)_第5张图片
放大后会有很多重复的recaculate style(重新计算样式)和layout(布局)。

前端性能指标和优化目标——布局(layout) 与绘制(paint)_第6张图片
此时发生了强制的回流(forced layout),这里在给宽度赋值之前先去取了元素的offsetTop,浏览器在帮我们提高布局性能的时候会尽量将修改布局相关的属性操作推迟,此时而当我们要去取布局相关的属性比如offsetTop时,浏览器就无法推迟,浏览器就不得不去立即进行新的计算以保证拿到最新的布局相关的属性值,所以这里在width赋值之前强制进行offset的计算,此时for循环会再继续这一轮计算-读-赋值的操作,每一轮浏览器都被强制立即计算新的offsetTop,这样就会有连续不断的强制回流发生,导致页面发生布局抖动,页面就会显得卡顿。

避免layout thrashing(布局抖动)

  • 读写分离:比如react的virtual dom,他会将对dom的操作进行批量的处理,先把读的操作都计算完,比如这里的offsetTop,然后统一做修改,比如这里的width。可以使用fastdom批量对DOM进行读写操作。
  • 避免回流:比如想改变某个元素的位置,不用top,left,right,bottom这些值,可以用transform-translate来做位移修改,3D动画属性transform-translate既不会触发回流,也不会发生重绘,它只会触发复合的过程,这样就能避免回流的高消耗了。

你可能感兴趣的:(前端性能优化,前端布局与绘制,前端性能优化,layout,paint)