(三)渲染优化

439 阅读15分钟

@TOC

浏览器渲染原理和关键渲染路径

浏览器是怎么把页面渲染出来的,渲染过程分很多环节,就是关键渲染路径,只有理解了渲染经历了什么步骤,才知道针对性的进行优化 网络资源被加载过来后,脚本、css都要进行解析,解析完之后浏览器要进行理解,如何把内容画到页面上,这就是渲染的过程 在这里插入图片描述 主线程有Recalculate style(计算样式)、Layout(布局)、Paint(绘制),这几个就是渲染过程中几个非常重要的阶段

浏览器的渲染流程

Javascript(触发视觉变化) 》Style(浏览器对样式重新进行计算) 》Layout(布局) 》Paint(绘制)》Composite(合成),这就是关键渲染路径,一共5步,无论是首次加载还是后面页面发生了样式上的变化,都要经历这几个步骤,最终把页面呈现给用户,理论上这5个步骤都是会被经历的,但是有些样式不会影响布局,也不会影响绘制,所以浏览器就进行了优化,如果是这样的样式,实际上可以不经历布局和绘制的过程,这样渲染就可以大大的被加速 Javascript,是可以通过Javascript实现一些页面视觉上的变化,例如添加dom元素,实现动画,还可以用css做动画,web animation api实现动画,这些都会触发视觉上变化 Style,浏览器对样式重新进行计算,这个过程会根据选择器进行重新匹配,计算哪些元素css受到影响,新的规则是什么样的,应该绘制成什么样子,每个元素绘制成什么样我们就清楚了 Layout,布局把你的元素按照你说的样式绘制到页面上,要把它绘制到页面上,这实际上是几何问题,需要知道元素的大小、位置 Paint,绘制,真正把内容画到页面上,画文字、图片、颜色、阴影等 Composite,合成,绘制会和这个合成联系在一起,浏览器为了提高效率,并不是把所有的东西都花在同一个层里,类似ps里,会建多个图层,最后再把它们组合起来,形成我们最后的一张图,浏览器为了提高效率也会把不同的东西画在不同的层上,最终再把它们合成在一起显示出来 当用户在地址栏输入一个地址,然后回车后,到页面显示出来之前,都经历了哪些过程 当浏览器拿到服务端返回的资源后,做了什么事? 无论js、css、html,都是代码,是文本,计算机理解不了文本,所以第一步它要通过一些解释器,把这些文本翻译成它能理解的数据结构 html是如何被转换的? 首先浏览器下载完html文档,就要把代码读进去,读进去的是文本,它先把这些文本转换成单个的字符;第二步,html里面有很多标签,标签是通过一对箭括号标记出来的,这个箭括号就可以用作识别,就可以把一些字符串理解成有含义的标记,这些标记最终被换成节点对象,放在链形数据结构里,链形数据结构就类似下图中的树,这就可以把html描述的嵌套关系很好的表达出来,通过这样一棵树,就可以把html的内容、属性、节点之间所有的关系都给表达清楚,这就叫DOM(文档对象模型),描述了html的结构 在这里插入图片描述 css部分如何被转换? 一样的道理,当解释器遇到你可能引用了web的css样式表,先把资源下载过来,下载完成后对这个资源进行文本处理,把里面的标记全部识别出来,看样式描述的是哪个节点的样式,然后也用树形结构把这个关系存储起来,如下图:除了描述节点之间的联系之外,还把每个节点所关联的样式给挂载起来 在这里插入图片描述

1.浏览器构建对象模型(两棵树)

构建DOM对象:HTML>DOM 构建CSSOM对象:CSS>CSSOM

2.浏览器构建渲染树

DOM(描述的是内容)和CSSOM(描述的是样式)合并成Render Tree,把内容和样式合在一起,让浏览器理解最终我们要把什么画在页面上,合并的结果如下图,把真正需要显示的东西留下,不需要显示的东西去掉,比如span节点的样式是diaplay:none,不需要显示在页面上,构造成渲染树后,span节点就会被去掉,最终只会留下需要显示到页面上的,有了这颗渲染树后,浏览器利用这棵树,知道每个节点什么尺寸,画在什么位置, 在这里插入图片描述

回流与重绘, 如何避免布局抖动

Layout(布局)与Paint(绘制)

是关键渲染路径中最重要的两个步骤,也是开销最高的两个步骤,如何减少或避免布局和绘制的发生?

  • 渲染树只包含网页需要的节点
  • 布局根据渲染树布局,计算每个节点精准的位置和大小-“盒模型”,关心位置和大小
  • 绘制是像素化每个节点的过程,把节点画在屏幕上 在我们页面中,这个关键路径至少会被走完一次,也就是最开始整个页面的加载 布局关心的是位置和大小,元素的几何信息,所以你的样式例如修改背景颜色不会修改位置和大小,是不会触发布局,关键渲染路径中Layout就可以跳过,直接到重绘 有没有即不会发生布局也不会发生重绘,是有的,有些动画是可以利用gpu加速,这种动画可以直接走复合的过程,不需要进行布局和重绘

影响回流的操作

布局也叫做回流,通常页面第一次加载完之后,把东西放在页面上我们叫做布局,如果是由于你之后页面上发生了视觉上的变化又导致再次布局,通常叫做回流

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

写个load事件,更改卡片的宽度,然后在performance里Timings那一栏load事件之后主线程有发生layout 在这里插入图片描述 如果一个回流操作不只影响本身,还会导致其他元素,甚至整个页面所有元素的位置都发生变化,这个消耗是非常高的,甚至页面会出现卡顿的状况

避免布局抖动(layout thrashing)

  • 避免回流 比如想改变元素位置,千万不要修改top、left这样的值,可以使用transform或者translate,通过translate做位移,这个3d动画既不会发生回流也不会发生重绘,只会触发复合的过程; 减少回流:react的v-dom减少回流,把一些你要会导致回流发生的操作,进行批量处理,积攒一些之后进行统一的计算,最后应用我们真正的dom上
  • 读写分离 批量的读操作完再进行批量写的操作 在这里插入图片描述

页面上所有的图片都开始进行动画的变化,发现动画并不流畅 在这里插入图片描述 性能分析里右上角红色三角形是表示发生了长任务 提示了强制回流,我们应该引起重视,这种是一个问题,问题出现在for循环里,给我们width进行赋值时,先取了offsetTop,浏览器为了提高布局的性能,会尽量把修改布局相关属性的操作推迟,但是什么情况是无法推迟呢?当你获得布局相关属性比如offsetTop时是无法推迟,不得不立即进行最新的计算,以保证你能取得最新的结果,所以在布局前它就被强制进行了一次计算,所以会先读这个值再对width写的操作,这是个循环,有连续的读写,而且每次读的步骤都会强制我们的布局立即进行重新的计算,导致有连续不断的回流发生,会导致页面的抖动,结果页面非常卡顿。

使用FastDom【防止布局抖动的利器】

在这里插入图片描述 measure测量(读),mutate修改(写),把一些读和写的操作进行分离,然后再通过调度函数去安排,批量进行读,批量进行写,达到消除页面抖动的效果 FastDom有提供相关的例子 在这里插入图片描述 使用Fastdom修改上面代码 在这里插入图片描述 运行后发现load之后没有再出现右上角红色三角形警告的长任务了,也未出现有问题的layout 在这里插入图片描述

使用FastDom批量对DOM的读写操作

什么是FastDom 如何使用FastDom的APIS

复合线程与图层【深入渲染流水线的最后一站】

复合线程(compositor thread)与图层(layers)

复合主要把我们的页面拆解成不同的图层,当我们的页面发生一些视觉变化时,有时这个变化可以只影响一个图层的变化,其他图层不需要受到影响,这样绘制的过程可以更高效完成,

复合图层做什么

  • 将页面拆分图层进行绘制再进行复合
  • 利用DevTools了解网页的图层拆分情况 页面是怎样拆成不同图层的,拆的规则是什么 默认情况下它是由浏览器决定的,浏览器会根据一些规则来判断是否要将页面去拆分成多个图层,又把哪些元素拆分成一个单独的图层,主要分析的是元素和元素之间是否有相互的影响,如果某些元素对其他元素造成的影响非常多,它就会被提取成一个单独的图层,这样的好处是如果它发生变化,只对它这个图层进行相关的重绘,而不会影响其他部分 我们也可以主动的把一些元素提取成一个单独的图层,我们知道这些元素会影响其他部分,我们把它提取出来,让它的变化变得更独立

在这里插入图片描述 左下角显示有两个图层,点击页面会显示出范围

  • 哪些样式仅影响复合,不触发布局和重绘 在这里插入图片描述 当我们使用这些属性时,如果可以所涉及到的元素提取到单独的图层,这些元素在发生动画或者视觉上变化时,就只会触发复合,不会触发布局和重绘 如下图,把卡片动画都拆成单独的图层,让gpu进行单独的处理 在这里插入图片描述

performance录制,移到卡片上的效果动画,如下图,发现会对样式进行重新计算,更新图层树,并没有发生布局和重绘,直接触发复合 在这里插入图片描述 但是如果把所有东西都拆到单独的图层中,也不行,图层越多,开销越高,会适得其反,会使页面所有操作非常慢,所以只把特定的,我们真正需要的,能达到这种效果的元素才提取到单独的图层

避免重绘【必学,加速页面呈现】

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 录制动画进行分析,4s左右点击触发了动画,触发动画后主线程开始繁忙,放大仔细看,有很多组,因为动画是持续不断的,我们可以看其中一组,首先会进行重新计算样式、更新图层树、再进行复合,所有任务执行得很快,没有长任务,也没有导致强制布局和布局抖动的问题 在这里插入图片描述 在这里插入图片描述 还有种方式看页面有没有发生重绘,修改样式如下,预计会发生布局和重绘 在这里插入图片描述 command+shift+p,勾选上Paint flashing,如果页面发生重绘,所重绘的区域会用绿色标记出来,非常方便我们观察页面有没有发生重绘 在这里插入图片描述 在这里插入图片描述 虽然tranform和opacity只影响复合,但是不要忘记做一件事,把它所影响到的元素提取到一个单独的图层,那是怎么做的? 在卡片的root样式类里有做个声明,利用了willChange属性,willChange值设置为transform,willChange: 'transform',这样浏览器就知道这个元素应当被提取到一个单独的图层里去进行, 在这里插入图片描述

减少重绘的方案

利用Devtools识别paint的瓶颈 利用will-change创建新的图层

import React from 'react';
import MaterialUICard from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import {withStyles} from '@material-ui/core/styles';
import './animation.css';
import {LazyLoadImage} from 'react-lazy-load-image-component';
import 'react-lazy-load-image-component/src/effects/blur.css';

const styles = theme => ({
    root: {
        margin: theme.spacing(1),
        willChange: 'transform'
    },
    card: {
        width: 300
    },
    cardSpinning: {
        width: 300,
        animation: '3s linear 1s infinite running rotate'
    },
    media: {
        height: 200,
        width: 300,
        objectFit: 'cover',
    }
});

class MyCard extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            spinning: false,
        }
    }

    spin = () => {
        this.setState({spinning: true});
    }

    render() {
        /*根据Spinning进行判断决定是否旋转*/
        let cardClass = this.state.spinning ? this.props.classes.cardSpinning : this.props.classes.card;
        return (
            <div className={this.props.classes.root}>
                {/*添加点击事件,触发动画*/}
                <MaterialUICard className={cardClass} onClick={this.spin}>
                    <LazyLoadImage
                        className={this.props.classes.media}
                        src={this.props.image}
                        effect="blur"
                        rel="preconnect"
                    />
                    <CardContent>
                        <Typography gutterBottom variant="h6" component="h2">
                            {this.props.title}
                        </Typography>
                        <Typography component="p">
                            {this.props.description}
                        </Typography>
                    </CardContent>
                    <CardActions className={this.props.classes.actions}>
                        <Button size="small" color="primary" variant="outlined">
                            联系方式:🐟🐡🐭🦆 Dereck
                        </Button>
                    </CardActions>
                </MaterialUICard>
            </div>
        );
    }
}

export default withStyles(styles)(MyCard);

高频事件防抖【解救页面卡顿的秘药】

高频 事件处理函数 防抖

高频 事件处理函数:有一些事件触发频率非常高,甚至会超过帧的刷新速率,比如滚动scroll、touchstart、touchmove、鼠标一类的mousemove等,这些函数触发频率非常快,快到肉眼看不出来,可以通过性能测量工具试试,把帧那一行和main函数里关于事件触发的这些任务对齐看下,很容易发现这类事件在一帧里会触发多次,导致在一帧里对这些事件要多次响应,如果事件处理函数里消耗比较高,那在一帧里任务会比较重,但是实际上并没有必要在一帧里处理多次,比如滚动,并不关心中间的过程,只关心最后滚动到哪里,而之前多出来的几次滚动造成任务量比较重,没有办法保证一帧能在16ms内完成,页面就会出现卡顿,也就是抖动 下面造一个pointer函数复现抖动问题 在这里插入图片描述 在这里插入图片描述 下面先看下一帧的生命周期,看下一帧是怎么被触发的 在这里插入图片描述 首先事件触发,然后js触发视觉上的变化,一帧开始,rAF调用,layout布局,paint重绘 rAF是在布局和重绘之前调用,这样可以利用rAF先把我们要做的处理先做完之后再去进行布局和绘制,极大的提高效率,另外rAF本身是由javascript进行调度的,会尽量让你能够在每一次绘制之前去触发这个rAF,尽量达到60fps的效果 rAF使用及防抖实现如下: 在这里插入图片描述

React时间调度实现【中高级前端需要了解的React调度原理】

基本原理

  • requestIdleCallback的问题 想模拟requestIdleCallback,requestIdleCallback是官方给出的标准,是另外一个函数,它的执行是希望在一帧16ms的时间内,如果还有空余的时间,可以让它做些事情,但是这个函数并没有被浏览器进行很好的支持,所以兼容性不好,react框架考虑到这点,并没有直接采用这个函数,而是通过rAF模拟实现rIC

  • 通过rAF模拟rIC 在这里插入图片描述 在这里插入图片描述 requestIdleCallback做什么,在什么时候做? 上图中很清晰的描述了一帧这样一个关键渲染周期内都做了什么事 requestAnimationFrame是在layout和paint之前被触发,这一帧要开始渲染之前 requestIdleCallback是在layout和paint之后被触发,这一帧已画完了,还有剩余的时间,可以做些额外的事情,但是这个事要有个度,因为要给主线程留更多的空余时间,要留出时间处理用户的交互,没办法预期什么时候用户会和你的页面进行交互,所以要尽量多的留出空闲时间,因为一旦有交互过来,我们至少要留50ms给每一次交互去做处理,所以react做这个时也会考虑这点,不会把所有的地方全占满,根据requestAnimationFrame可以算出requestIdleCallback的时间,requestAnimationFrame是在一帧开始的时候,这一帧也有上限时间,根据这两个时间就可以计算出requestIdleCallback的时间 在用户不再看这个页面,或者说现在页面不可见,属于后台状态时,requestAnimationFrame实际上不会运行,react只是借用这个函数,需要让任务即使是在后台状态时还要继续完成,所以需要找个替代方案,能保证在后台继续把任务做完,所以如下图用setTimeout做一个替代方案实现图 作为调度函数,最关心的是所有任务,会给这些任务安排优先级,react这边安排了5个优先级,从立即可以执行到有空闲执行的,另外这些任务都有个过期时间,还有就是这些任务的存储,肯定有一个队列,把这些任务排到队列里,然后等待IdleCallback,有空闲时间时去执行,底层的实现是一个双向的环形链表,如何利用环形链表去安排整个任务的优先级 在这里插入图片描述