前端性能优化之渲染优化

页面渲染过程

为了使每一帧页面渲染的开销都能在期望的时间范围内完成。就需要开发者了解渲染过程的每个阶段,以及各阶段中有哪些优化空间是我们力所能及的。经过分析根据开发者对优化渲染过程的控制力度,可以大体将其划分为5各部分:js处理、计算样式、页面布局、绘制与合成。这个过程中的每一个阶段都有可能产生卡顿。注意:并非对于每一帧画面都会经历这5个部分,比如仅修改与绘制相关的属性(文字颜色,背景图片或边缘阴影等),而未对页面布局产生任何修改,那么在计算样式阶段完成后,便会跳过页面布局直接执行绘制。如果所更改的属性既不影响页面布局又不需要重新绘制,便可直接跳到合成阶段执行。具体修改哪些属性会触发页面布局、绘制或合成阶段的执行,这与浏览器的内核存在一定关系。

属性BlinkGeckoWebkit
z-index绘制/合成绘制/合成布局/绘制/合成
transform合成合成布局/绘制/合成
opacity绘制/合成合成布局/绘制/合成
min-width布局/绘制/合成布局/合成布局/绘制/合成
color布局/绘制布局/绘制布局/绘制/合成
background布局/绘制布局/绘制布局/绘制/合成
border-radius布局/绘制布局/绘制布局/绘制/合成
border-style布局/绘制/合成布局/绘制/合成布局/绘制/合成
border-width布局/绘制/合成布局/绘制/合成布局/绘制/合成

Google的chrome实验室在网站上列出了许多css属性的详细表现,可以自行查看。

js执行优化

实现动画效果

推荐使用requestAnimationFrame方法来实现动画效果,requestAnimationFrame方法的执行时机会与系统的刷新频率同步。这样就能保证回调函数在屏幕的每次刷新间隔中只被执行一次,从而避免因随机丢帧而造成的卡顿现象。

其使用方法也十分简单,仅接受一个回调函数作为入参,即下次重绘之前更新动画帧所调用的函数。返回值为一个long类型整数,作为回调任务队列中的唯一标识,可将该值传给window.cancelAnimationFrame来取消回调,以某个目标元素的平移动画为例:

let start;
//定义目标动画元素
const element = document.getElementById("MyAnimate");
element.style.position = 'absolute'
//定义动画回调函数
function updateScreen(timestamp){if(!start) start = timestamp;//根据时间戳计算每次动画位移const progress = timestamp-start;element.style.left = "12px"if(progress<2000) window.requestAnimationFrame(updateScreen)
}
//启动动画回调函数
window.requestAnimationFrame(updateScreen)

除了通过让回调函数的触发时机与系统刷新频率同步来消除动画的丢帧卡顿,requestAnimationFrame方法还能通过节流不必要的函数执行,来帮助cpu的节能。
具体而言,对于cpu节能方面,考虑当浏览器页面最小化或被隐藏起来时,动画对用户来说是不可见的,那么刷新动画所带来的页面渲染就是对cpu资源的浪费,完全没有意义。
当创建setInterval定时器后,除非显式调用clearInterval去销毁该定时器,不然在后台的动画任务会不断执行,而requestAnimationFrame方法则完全不同,当页面未被激活时,屏幕刷新任务会被系统暂停,只有当页面被激活时,动画任务才会被激活并从上次暂停的地方继续执行,所以能有效地节省cpu开销。
在页面地一些高频事件中,比如页面滚动的scroll、页面尺寸更改的resize,需要防止在一个刷新时间间隔内发生多次函数执行,也就是所谓的函数节流。对60hz的显示器来说,差不多每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来,所以requestAnimationFrame方法仅在每次刷新周期中执行一次函数调用,既能保证动画的流畅性又能很好地节省函数执行地冗余开销。

恰当使用web worker

js是单线程执行的,所有任务放在一个线程上执行,只有当前一个任务执行完才能处理后一个任务,不然后面的任务只能等待,这就限制了多核计算机充分发挥它的计算能力。同时在浏览器上,js的执行通常位于主线程,这恰好与样式计算、页面布局以及绘制一起,如果js运行时间过长,必然就会导致其他工作任务的阻塞而造成丢帧。
为此可将一些纯计算的工作迁移到web worker上处理,它为js的执行提供了多线程环境,主线程通过创建出worker子线程,可以分担一部分自己的任务执行压力。在worker子线程上执行的任务不会干扰主线程,待其上的任务执行完成后,会把结果返回给主线程,这样的好处是让主线程可以更专注地处理ui交互,保证页面的使用体验流程。需要注意的是,worker子线程一旦创建成功就会始终执行,不会被主线程上的事件所打断,这就意味着worker会比较耗费资源,所以不应当过度使用,一旦任务执行完毕就应及时关闭。除此之外,在使用中还有以下几点应当注意。

  • dom限制:worker无法读取主线程所处理网页的dom对象,也就无法使用document、window和parent等对象,只能访问navigator和location对象。
  • 文件读取限制:worker子线程无法访问本地文件系统,这就要求所加载的脚本来自网络。
  • 通信限制:主线程和worker子线程不在同一个上下文内,所以它们无法直接进行通信,只能通过消息来完成。
  • 脚本执行限制:虽然worker可以通过xmlhttprequest对象发起ajax请求,但不能使用alert和confirm方法在页面中弹出提示。
  • 同源限制:worker子线程执行的代码文件需要与主线程的代码文件同源。

web worker的使用方法非常简单,在主线程中通过new Worker()方法来创建一个worker子线程,构造函数的入参是子线程执行的脚本路径,由于代码文件必须来自网络,所以如果代码文件没能下载成功,worker就会失败。

//创建子线程
const worker = new Worker("demo_worker.js");
//主线程向子线程发生消息
const dataToWorker = {//...
}
worker.postMessage(dataToWorker);
//接下来主线程就可以继续其他构造,只需通过监听子线程返回的消息再进行相应处理
worker.addEventListener('message',(event)=>{const workedData = event.data;//将数据更新到屏幕上
})

在子线程处理完相关任务后,需要及时关闭worker子线程以节省系统资源,关闭的方式有两种:在主线程中通过调用worker.terminate方法来关闭;在子线程中通过调用自身全局对象中的self.close方法来关闭。

考虑到上述关于web worker使用中的限制,并非所有任务都适合采用这种方式来提升性能。如果所要处理的任务必须要放在主线程上完成,则应当考虑将一个大型任务拆分为多个微任务,每个微任务处理的耗时最好在几毫秒之内。能在每帧的requestAnimationFrame更新方法中处理完成。

//将一个大型任务拆分为多个微任务
const taskList = splitTask(BigTask);
//微任务处理逻辑,入参为每次任务起始时间戳
function processTaskList(taskStartTime){let taskStartTime;do{//从任务堆栈中推出要处理的下一个任务const nextTask = taskList.pop();//处理下一个任务processTask(nextTask);//获取任务执行完成的时间,如果时间够3毫秒就继续执行taskFinishTime = window.performance.now();}while(taskFinishTime-taskStartTime < 3);//如果任务堆栈不为空则继续if(taskList.length>0){requestAnimationFrame(processTaskList)}
}
requestAnimationFrame(processTaskList)

事件节流和事件防抖

所谓事件节流,简单来说就是在某段时间内,无论触发多少次回调,在计结束后都只响应第一次的触发。以scroll事件为例,当用户滚动页面触发了一次scroll事件后,就为这个触发操作开启一个固定时间的计时器。在这个计时器呈持续时间内,限制后续发生的所有scroll事件对回调函数的触发,当计时器计时结束后,响应执行第一次触发scroll事件的回调函数。

function throttle(time,callback){//上次触发回调的时间let last = 0;//事件节流操作的闭包返回return(params)=>{let now = Number(new Date())if(now-last >=time){//超出节流时间间隔,触发响应回调函数last = now;callback(params);}}
}
const throttle_scroll = throttle(1000,()=>console.log('页面滚动'))
document.addEventListener('scroll',throttle_scroll )

事件防抖的实现与事件节流类似,只是所响应的触发事件是最后一次事件。具体来说,首先设定一个事件防抖的时间间隔,当时间触发开始后启动计时器,若在定时器结束计时之前又有相同的事件被触发,则更新计时器但不响应回调函数的执行,只有当计时器完整计时结束后,才去响应执行最后一次事件触发的回调函数。

function throttle(time,callback){//上次触发回调的时间let last = 0,timer = null;//事件节流操作的闭包返回return(params)=>{let now = Number(new Date())if(now-last >=time){//超出节流时间间隔,触发响应回调函数last = now;callback(params);}else{//重新设置防抖定时器clearTimeout(timer);timer = setTimeout(()=>{last = now;callback(params)},time)}}
}
const throttle_scroll = throttle(1000,()=>console.log('页面滚动'))
document.addEventListener('scroll',throttle_scroll )

计算样式优化

在js处理过后,若发生了添加和删除元素,对样式属性和类进行了修改,就都会导致浏览器重新计算所涉及到的元素的样式,某些修改还可能会引起页面布局的更改和浏览器的重新绘制。

  • 减少要计算样式的元素数量
    css引擎查找样式表时,对每条规则的匹配顺序是从右往左的。
    .product-list li{}
    
    如上面例子的css引擎需要首先遍历页面上的所有li标签元素,然后确认每个li标签有包含类名未product-list的父元素才是目标元素。所以为了提高页面的渲染性能,计算样式阶段应当尽量减少参与样式计算的元素数量。
  • 降低选择器的复杂性
    尽量使用id或者类选择器标识元素
  • 使用BEM规范
    参考资料:https://www.jianshu.com/p/6b3535635b2e

页面布局与重绘的优化

页面布局也叫做重排和回流,指的是浏览器对页面元素的集合属性进行计算并将最终结果绘制出来的过程。凡是元素的宽高尺寸、在页面中的位置以及隐藏或显示等信息发生改变时,都会触发页面的重新布局。
通过页面布局的作用范围会涉及整个文档,所以这个环节会带来大量的性能开销,开始时应当从代码层面出发,尽量避免页面布局或最小化其处理次数。如果仅修改了dom元素的样式,而未影响其几何属性,则浏览器会跳过页面布局的计算环节,直接进入重绘阶段。
虽然重绘的性能开销不及页面布局高,但为了更高的性能体验,也应当降低重绘发生的频率个复杂度。

  • 触发页面布局与重绘的操作
    要想避免或减少页面布局与重绘的发生,首先就是需要知道有哪些操作能够触发浏览器的页面布局与重绘的操作,然后在开发过程中尽量去避免。
    这些操作大致可以分为三类:首先就是对DOM 元素几何属性的修改,这些属性包括width、height、padding、marginleft、top等,某元素的这些属性发生变化时,便会波及与它相关的所有节点元素进行几何属性的重新计算,这会带来巨大的计算量;其次是更改DOM 树的结构,浏览器进行页面布局时的计算顺序,可类比树的前序遍历,即从上向下、从左向右。
    这里对DOM 树节点的增、删、移动等操作,只会影响当前节点后的所有节点元素,而不会再次影响前面已经遍历过的元素;最后一类是获取某些特定的属性值操作,比如页面可见区域宽高offsetWidth、offsetHeight, 页面视窗中元素与视窗边界的距离offsetTop、 offsetLeft,类似的属性值还有ScrollTop、 scrollLeft, scrollWidth, scrolHeight,clientWidth、clientHeight 及调用 window.getComputedStyle方法。clientTop
    这些属性和方法有一个共性,就是需要通过即时计算得到,所以浏览器就需要重新进行页面布局计算。

  • 避免对样式的频繁改动
    在通常情况下,页面的一帧内容被渲染到屏幕上会按照如下顺序依次进行,首先执行JavaScript代码,然后依次是样式计算、页面布局、绘制与合成。如果在JavaScript运行阶段设计上述三类操作,浏览器就会强制提前页面布局的执行,为了尽量降低页面布局计算带来的性能损耗,应当避免使用js对样式进行频繁修改。

    1. 使用类名对样式逐条修改
      在JevaScript代码中逐行执行对元素样式的修改,是一种糟糕的编码方式,对未形成编码规范的前端初学者来说经常会出现这类的问题。错误代码示范如下:

      //获取dom元素逐行修改样式
      const div =document.getElementByid('mydiv');div.style.height ='100px'
      div.style.width ='100px'
      div.style.border-t2px solid blue'
      

      上述代码对样式逐行修改,每行都会触发一次对渲染树的更改,于是会导致页面布局重新计算而带来巨大的性能开销。合理的做法是,将多行的样式修改合并到一个类名中,仅在JavaScript 脚本中添加或更改类名即可。CSS类名可预先定义:

      .my-div {height:100px;width:100px;border:2px solid blue;}
      

      然后统一在JavaScript 中通过给指定元素添加类的方式一次完成,这样便可避免触发多次对页面布局的重新计算:

      const div =document.getElementById ('mydiv');mydiv.classList.add ('my-div');  
      
    2. 缓存对敏感属性值的计算
      有些场景我们想要通过多次计算来获得某个元素在页面中的布局位置,比如:

        const list = document .getElementByid('list) ;for (let i =0;i<10;i++{list.style.top='$(list.offsetrop+10}px';}
      

    这不但在赋值环节会触发页面布局的重新计算,而且取值涉及即时敏感属性的获取,如offsetTop和offsetLeft,也会触发页面布局的重新计算。这样的性能是非常糟糕的,作为优化我们可以将敏感属性通过变量的形式缓存起来,等计算完成后再统一进行赋值触发布局重排。

      const list = document .getElementByid('list) ;//将敏感属性缓存起来let offsetTop = list.offsetTop;for (let i =0;i<10;i++{offsetTop +=10;}//计算完成后统一赋值触发重排list.style.top=offsetTop
    
  1. 使用requestAnimationFrame方法控制渲染帧
    前面讲js动画时,提到了该方法可以控制回调在两个渲染帧之间仅触发一次,如果在其回调函数中一开始就取值到即时敏感属性,其实获取的是上一帧旧布局的值,并不会触发页面布局的重新计算。
//在帧开始时触发回调
requestAnimationFrame(test())
function test(){const div = document.getElementById("div");console.log(div.offsetHeight);
}

如果在请求此元素高度之前更改其样式,浏览器就无法直接使用上一帧的旧有属性值,而需要先应用更改的样式,再运行页面布局计算后才能返回所需的正确高度值。这样的多余开销显然是没有必要的。因此考虑到性能因素,在requestAnimationFrame方法的回调函数中,应始终优先样式的读取,然后再执行相应的写操作:

function test(){const div = document.getElementById("div");console.log(div.offsetHeight);div.classList.add("my-div")
}

合成处理

合成处理是将已绘制的不同图层放在一起,最终在屏幕上渲染出来的过程。在这个环节中,有两个因素可能会影响页面性能:一个是所需合成的图层数量,一个是实现动画的相关属性。

  • 新增图层
    在降低绘制复杂度小节中讲到,可通过将固定区域和动画区域拆分到不同图层上进行绘制,来达到绘制区域最小化的目的。
.new-layer{will-change:transform;
}

will-change在chrome、火狐以及opera上均有效,而对于safari等不支持的浏览器,可以是3d变化来强制创建:

.new-layer{transform:translate(0);
}
  • 仅与合成相关的动画属性
    如果一个动画的实现不经过页面布局和重绘环节,仅在合成处理阶段就能完成,则将会节省大量的性能开销。目前能够符合的动画属性:透明度opacity和图层变换transform。

在使用这两个属性实现相应的动画效果时,需要注意动画元素应当位于独立的会图层上,以避免影响其他绘制区域。这就需要将动画元素提升至一个新的会图层。

最后,多使用chrome的开发者工具对渲染优化进行评估。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/613140.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Farad capacitor法拉电容优点及缺点

Farad capacitor 法拉电容又称Electrical Double-Layer Capacitor双电层电容器、Gold capacitor黄金电容、Super capacitor 超级电容器&#xff0c;是一种化学元件。Super capacitor 超级电容器通过极化电解质来储能&#xff0c;但不发生化学反应&#xff0c;而且储能过程是可逆…

如何在 Windows10 下运行 Tensorflow 的目标检测?

看过很多博主通过 Object Detection 实现了一些皮卡丘捕捉&#xff0c;二维码检测等诸多特定项的目标检测。而我跟着他们的案例来运行的时候&#xff0c;不是 Tensorflow 版本冲突&#xff0c;就是缺少什么包&#xff0c;还有是运行官方 object_detection_tutorial 不展示图片等…

Leetcode160 两个链表是否相交

leetcode 160题&#xff0c;判断两个链表是否相交 此题可以说是算法界第一深情&#xff0c;如果我走过你走过的路&#xff0c;那么我们就可能会相遇。 具体解决思路如下 两个链表是否相交有两种可能&#xff0c;一种不相交&#xff0c;一种相交&#xff0c;首先来看下相交的…

golang学习-指针

1、定义 指针也是一个变量&#xff0c;但它是一个特殊的变量&#xff0c;它存储的是另一个变量的内存地址。是引用数据类型。 取一个变量的地址&#xff1a;&a 定义&#xff1a; var p *int &a 可以理解为 指针变量p中存储的是a的内存地址&#xff0c;但是变量p也…

element-plus 、element-ui —— Cascader 勾选内容后自动关闭内容选择面板

最近工作需求是 el-Cascader 级联多选框 任意选择一项&#xff0c;自动关闭面板&#xff1f; 来总结记录下。 element-ui的关闭面板方式&#x1f447; this.$refs.CascaderRef.dropDownVisible false element-plus的关闭面板方式&#x1f447; vue3.0写法 CascaderRef.val…

leaflet学习笔记-贝塞尔曲线绘制(八)

前言 两点之间的连线是很常见的&#xff0c;但是都是直直的一条线段&#xff0c;为了使连线更加平滑&#xff0c;我们可以使用曲线进行连线&#xff0c;本功能考虑使用贝塞尔曲线进行连线绘制&#xff0c;最后将线段的两端节点连接&#xff0c;返回一个polygon。 贝塞尔简介 …

大模型学习之书生·浦语大模型4——基于Xtuner大模型微调实战

基于Xtuner大模型微调实战 Fintune简介 海量数据训练的base model指令微调Instructed LLM 增量预训练微调 增量数据不需要问题&#xff0c;只需要答案&#xff0c;只需要陈述类的数据 指令跟随微调 指定角色指定问题给对应的user指定答案给assistant LIaMa2InternLM 不同的模…

金三银四-JVM核心知识高频面试题

又要快到一年一度的金三银四&#xff0c;开始复习啦&#xff5e;&#xff01; 每天一点点。。 目录 一、JVM中的垃圾收集器有哪些&#xff0c;它们的工作原理是什么&#xff1f; 二、JVM中的类加载器有哪些&#xff0c;它们各自的作用是什么&#xff1f; 三、JVM中垃圾回收的…

Linux第24步_安装windows下的VisualStudioCode软件

Windows下的VSCode安装后&#xff0c;还需要安装gcc编译器和g编译器。 gcc&#xff1a;编译C语言程序的编译器&#xff1b; g&#xff1a;编译C代码的编译器&#xff1b; 1、在Windows下安装VSCode&#xff1b; 双击“VSCodeUserSetup-x64-1.50.1.exe”,直到安装完成。 2、…

c++学习笔记-STL案例-演讲比赛管理系统1

目录 1演讲比赛需求 1.1 比赛规则 1.2 程序功能 2.项目创建 2.1 创建新项目 2.2 添加文件 3.3 文件添加成功 3.创建管理类 3.1 功能描述 3.2 创建文件 4 菜单功能 4.1 功能描述 4.2 添加成员函数 4.3 菜单功能实现 4.4 main()函数中调用 4.5 实现结果 5 退出系…

C++力扣题目104--二叉树的最大深度

给定一个二叉树&#xff0c;找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 说明: 叶子节点是指没有子节点的节点。 示例&#xff1a; 给定二叉树 [3,9,20,null,null,15,7]&#xff0c; 返回它的最大深度 3 。 思路 看完本篇可以一起做了如下…

【C++11/C++17】左值与右值

左值和右值是C中的两个重要概念&#xff0c;它们涉及到变量的存储位置和生命周期。 左值&#xff08;Lvalue&#xff09; 左值表示一块具有确定地址的内存单元。它表示一个具体的、明确存在的存储单元。可以被取地址运算符&#xff08;&&#xff09;获取其地址。变量、数…

pytorch详细探索各种cnn卷积神经网络

目录 torch.nn.functional子模块详解 conv1d 用法和用途 使用技巧 适用领域 参数 注意事项 示例代码 conv2d 用法和用途 使用技巧 适用领域 参数 注意事项 示例代码 conv3d 用法和用途 使用技巧 适用领域 参数 注意事项 示例代码 conv_transpose1d 用法…

electron+vue编辑Office Word?

Electron 桌面应用是基于 Chromium 内核的&#xff0c;而谷歌Chrome取消了对PPAPI插件支持后&#xff0c;在线Office方案纷纷失效&#xff0c;只能更换国产浏览器在低版本浏览器苟延残喘&#xff0c;不能用于electronvue项目。 经过小编不断的寻找&#xff0c;终于找到一款至今…

redis主从复制、哨兵与集群

目录 一、概述 二、redis主从复制 1、概念 2、主从复制的作用 3、主从复制流程 4、搭建Redis 主从复制实验 ①在三台服务器上安装redis &#xff08;1&#xff09;关闭防火墙和安全机制 &#xff08;2&#xff09;修改内核参数 &#xff08;3&#xff09;安装redis …

自旋框的使用

1. 自旋框 实例化 //实例化单精度自旋框QSpinBox* spinBox new QSpinBox(this);//实例化双精度自旋框QDoubleSpinBox* doubleSpinBox new QDoubleSpinBox(this);1.1 单精度自旋框 QSpinBox 1.1.1 单精度自旋框的基本函数 QSpinBox_QDoubleSpinBox Dialog.cpp #include "…

网络安全保险发展起始阶段的挑战及应对措施

文章目录 前言一、网络安全保险的有序发展二、当前我国网络安全保险发展的初期态势&#xff08;一&#xff09;网络安全风险类型&#xff08;二&#xff09;网络安全保险的作用&#xff08;三&#xff09;与外国网络安全保费的规模对比 三、我国网络安全保险发展初期面临的挑战…

vue中常用的指令修饰符

vue中常用的指令修饰符有哪些 在Vue.js中&#xff0c;指令修饰符用于在指令后面以点号&#xff08;.&#xff09;形式添加特殊后缀&#xff0c;以表示对指令的特定行为或修饰。 以下是一些常用的Vue.js指令修饰符&#xff1a; 1. v-on &#xff08;&#xff09;指令修饰符&am…

vue/vue3/js来动态修改我们的界面浏览器上面的文字和图标

前言&#xff1a; 整理vue/vue3项目中修改界面浏览器上面的文字和图标的方法。 效果&#xff1a; vue2/vue3: 默认修改 public/index.html index.html <!DOCTYPE html> <html lang"en"><head><link rel"icon" type"image/sv…

报考PMI-ACP总费用是多少?费用明细!

ACP认证是由美国项目管理协会&#xff08;PMI&#xff09;推出的针对敏捷项目管理专业人士的资格认证&#xff0c;由于其高含金量受到了不少小伙伴的追捧。那么这个证书考试费多少钱呢&#xff1f;贵不贵呢&#xff1f;我们来一起探讨下。 PMI-ACP认证考试费用分为三个部分&am…