当前位置: 首页 > Web前端 > vue.js

web3下的性能优化(渲染方向)

时间:2023-04-01 00:20:31 vue.js

性能优化(渲染级别)这里针对浏览器渲染过程的工作流程进行一些优化。有一个宏观的概念...先回顾一下渲染过程Workflow渲染过程Workflow1.DOMTree构建当渲染过程收到导航确认信息,开始接受HTML数据时,主线程会将文本字符串解析成DOM;这里依赖HTMl解析器:接受字节流->维护令牌栈->生成节点节点->形成DOM;遇到内嵌脚本,DOM解析工作停止;js引擎介入执行(可能修改dom结构);执行完js后继续解析工作,所以js会阻塞dom解析。遇到其他内联资源(css、img),会通知网络进程下载,尤其是css;js在操作dom样式时会依赖cssom,生成layoutTree也需要cssom;所以css会阻塞js2.style计算的执行,这里构建cssom(css规则树)会根据css选择器解析css,得到每个节点最终的计算style值;与DOM树构建并行;对应于styleSheets3。计算布局,生成布局树来渲染一个完整的页面,除了要知道每个节点的具体样式外,还需要知道每个节点在页面中的位置。布局其实就是寻找所有元素的几何关系的过程。这里通过遍历DOM和相关元素的计算样式,主线程会构建一个包含每个元素的坐标信息和框大小的布局树。布局树类似于DOM树,但它只包含页面上可见的元素。如果一个元素被设置为`display:none`,这个元素将不会出现在布局树上。伪元素虽然在DOM树上是不可见的,但是在布局树中是可见的。4.分层,绘制(layer->paint)为特定节点生成专用图层(will-change属性),生成图层树;为图层生成绘图表(记录绘图指令和顺序),提交给合成线程5.分块、栅格化合成线程将图层分成瓦片,栅格化生成位图(GPU进程)6.合成,显示一张绘图tile栅格化后会生成command,通过IPC提交给浏览器,浏览器进程执行,绘制到内存中显示在显示器上。这里我们对每个步骤进行分析,尽可能提炼出一些优化方法。1、首屏优化(让浏览器接收尽可能完整的HTML字符串)目前我们都习惯使用vue、react等框架。大部分情况下,我们是基于spa(即纯客户端渲染csp)的单页应用,依赖js运行并输出渲染页面,这样在第一次加载的时候,我们需要的是显然不可取,等待所有资源加载完毕才显示页面,增加了等待时间,这对于C端业务(尤其是有SEO需求的)显然不可取;所以后来提出了服务端渲染(ssr同构),由服务端给出完整的html,由客户端接管后续的操作。优化措施一:服务端渲染请参考vuenuxt.js和Vue官网的ssr指南。优化措施二:预渲染服务端渲染需要大量的服务器开销。如果预算不足,尽量不要使用,或者少量页面使用SSR;这时候不妨考虑预渲染。主要是针对一些纯静态页面prerender-spa-plugin优化措施3:增加请求资源过程中的交互体验,如加载、转场动画、骨架屏等2、DOM解析优化(让html解析流畅)CSS和JS在解析HTML字符串时穿插。我们知道,同时工作的只有js线程和渲染线程中的一个。jsjs的执行会阻塞DOM解析;另外js的执行会依赖cssom(js可能会操作样式),所以加载css会阻塞js;而且layouttree最终的合成也依赖于cssom和dom;因此,js和css的加载顺序也是一个可以优化的点。css加载1.cssimportcss一般是通过head中的link和style标签引入,或者内联到标签中,尽量提前放置并尽快加载2.css写法(cssselector来自规则对于从右到左的匹配应该尽可能简单,避免层次嵌套;少用*、标签选择器等;考虑属性继承js加载1.正常加载1.异步方式加载1.defermodeloading从应用的角度来看:当我们的脚本不强依赖DOM元素并且其他脚本,我们会选择async;当脚本依赖DOM元素和其他脚本的执行结果时,我们会选择defer3。DOM操作优化——当我们使用JS操作DOM时,本质上是JS引擎和渲染引擎之间存在通信,同样会消耗性能;另外,我们对DOM的操作不会仅限于访问,而是修改。当我们对DOM的修改导致其外观(样式)发生变化时,将触发回流或重绘。导致布局树发生变化...重排:当我们对DOM的修改导致DOM的几何尺寸发生变化时(比如修改元素的宽度、高度或者隐藏元素等),浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会受到影响),然后绘制计算结果。这个过程就是重排(也叫回流)。重绘:当我们对DOM的修改导致样式改变但不影响其几何属性(如修改颜色或背景色)时,浏览器不需要重新计算元素的几何属性,直接绘制新的对于元素样式(跳过上面显示的重新排列)。这个过程称为重绘。优化措施一:减少DOM操作和虚拟DOM的方法类似,js批量操作需要更新dom,然后一次性更新到DOM树中。使用DocumentFragment优化措施二:减少重绘和重排,改变DOM元素的几何属性当一个DOM元素的几何属性发生变化时,与其相关的所有节点的几何属性(如父子节点、兄弟节点、etc.)需要重新计算,会带来巨大的计算量...常见的几何属性有width、height、padding、margin、left、top、border等,这里不一一列举我们可以赋值:constcontainer=document.getElementById('container')container.style.width='100px'container.style.height='200px'container.style.border='10pxsolidred'container.style.color='red'//优化,切换类名而不是设置value.basic_style{width:100px;高度:200px;边框:10px纯红色;颜色:红色;}constcontainer=document.getElementById('container')container.classList.add('basic_style')当你想使用这样的属性时减少位置值的获取:,客户端高度;这些属性值需要即时计算。因此,为了获得这些值,浏览器也会进行重排。//我们可以缓存这些值//缓存offsetLeft和offsetTop值constel=document.getElementById('el')letoffLeft=el.offsetLeft,offTop=el.offsetTop//在JS层面计算for(leti=0;i<10;i++){offLeft+=10offTop+=10}//一次性将计算结果应用到DOMel.style.left=offLeft+"px"el.style.top=offTop+"px"使用GPU层,比如使用经验css3属性变换等,使用GPU进程处理4.DOM操作优化2(异步更新策略)使用事件循环(EventLoop)进行更新尽可能早的DOM;事件循环下的任务队列事件循环中有两种异步队列:宏(宏任务:浏览器维护)队列和微(微任务:js引擎维护)队列。Macro-Task宏任务setTimeout、setInterval、setImmediate、script(整体代码)、I/O操作、UI渲染等Micro-Taskprocess.nextTick、Promise、MutationObserver等事件循环运行过程1.初始状态:调用栈为emptymicroqueue空,宏队列中只有一个脚本(全局全局代码)2.全局上下文(脚本标签)压入调用栈,代码同步执行。在执行过程中,可以通过调用一些WebAPI接口产生新的宏任务和微任务,并将它们推入各自的任务队列中。同步代码执行完毕后,脚本脚本将从宏队列中移除。这个过程本质上就是队列宏任务执行和出队的过程。3、我们前面出队的是一个宏任务,但是这一步我们处理的是一个微任务。但需要注意的是,当宏任务出队时,任务是一个一个执行的;当微任务出队时,任务被一个接一个地执行。(当前出队的宏任务下的微任务队列)因此,我们在处理微队列的时候,会将队列中的任务一个一个执行,出队,直到队列清空。4.执行渲染操作,更新界面(重点)。5、检查是否有webworker任务,如果有则处理。6.重复上述过程,直到两个队列都清空,异步更新DOM。如果我想在一个异步任务中更新DOM,我应该把它打包成一个微程序还是一个宏程序?//假设被打包成宏任务//任务是修改DOM的回调setTimeout(task,0)/*现在任务被压入宏队列。但是因为script脚本本身就是一个宏任务,所以这次script脚本执行完之后,接下来就是处理微队列,然后执行一次渲染操作,进入下一轮事件循环。..此时我们任务执行的时机是在下一轮事件循环中*///假设被打包成一个microtaskPromise.resolve().then(task)/*script脚本执行完后,它会立即处理微任务队列;微任务处理完成后,修改DOM;然后就可以开始渲染过程了……这样就不需要消耗额外的渲染,不需要等待另一轮事件循环,直接呈现给用户最即时的更新结果。*/所以当我们需要在异步任务中实现DOM修改时,需要封装成微任务。这可以减少实际渲染时间。Vue的批量异步状态更新:nextTick详见我之前写的nextTick,虽然不完整;至少我们知道我们可以使用微任务来优化DOM操作更新5.懒加载方法//lazy-loading//获取所有图片标签constimgs=document.getElementsByTagName('img')//获取图片的高度可见区域constviewHeight=window.innerHeight||document.documentElement.clientHeight//num用于统计当前显示的是A图的位置,避免每次从第一张图开始检查是否暴露letnum=0functionlazyload(){for(leti=num;i=0){//将真实的src写入显示图像imgs[i]的元素。i+1张开始检查是否有num=i+1}}}//监听Scroll事件window.addEventListener('scroll',lazyload,false);6.scroll事件、resize事件、Mouse事件(如mousemove、mouseover等)和键盘事件(keyup、keydown等)可能被用户频繁触发的事件防抖和节流;频繁触发回调导致大量计算导致页面抖动甚至卡顿。为了规避这种情况,我们需要一些手段来控制事件被触发的频率。这样throttle(事件节流)和debounce(事件防抖)就出现了;这两个函数以闭包的形式存在。他们包装事件对应的回调函数,以自由变量的形式缓存时间信息,最后通过setTimeout来控制事件的触发频率。Throttle一段时间,不管触发多少次回调,我只识别第一次,计时结束时调用//fn是我们需要wrap的事件回调,interval是时间间隔的阈值functionthrottle(fn,interval){//last是上次触发回调的时间letlast=0//将throttle处理结果作为函数返回returnfunction(){//调用时保留thiscontextletcontext=this//调用时不断传入入参letargs=arguments//记录本次触发回调的时间letnow=+newDate()//判断上次触发的时间是否与本次触发的时间差本次触发时间小于时间间隔阈值if(now-last>=interval){//如果时间间隔大于我们设置的时间间隔阈值,则执行回调last=now;fn.apply(上下文,参数);}}}//使用throttle包裹滚动回调,每1s触发一次constbetter_scroll=throttle(()=>console.log('triggeredascrollevent'),1000)document.addEventListener('scroll',better_scroll)debounce一段时间,不管你触发回调多少次,我只认最后一次//fn是我们需要包装的事件回调,delay是每次延迟执行函数的等待时间debounce(fn,delay){//timerlettimer=null//将debounce处理结果作为函数使用returnreturnfunction(){//调用时保留this上下文letcontext=this//调用letargs时保留传入的参数=arguments//每触发一个事件,清除之前的老定时器if(timer){clearTimeout(timer)}//设置一个新的定时器timer=setTimeout(function(){fn.apply(context,args)},delay)}}//使用debounce包装滚动回调//trigger事件后,1s内再次触发则不会执行。constbetter_scroll=debounce(()=>console.log('scrolleventtriggered'),1000)document.addEventListener('scroll',better_scroll)参考DocumentFragmentMDN