当前位置: 首页 > 科技观察

如何优化前端性能?

时间:2023-03-16 14:15:06 科技观察

随着前端的范围逐渐扩大,深度逐渐下沉,丰富的前端必然带来的问题之一就是性能。尤其是在大型复杂的项目中,繁重的前端业务可能会因为一个小的数据依赖导致整个页面卡顿甚至崩溃。本文基于QuickBI(数据可视化分析平台)多年来在架构变化中的性能调查、解决和总结的“个性”问题,试图总结整个前端相对“通病”的问题-end级别,并提供一些前端性能解决方案。1、性能问题的原因?性能问题的成因通常不是单方面的,尤其是经过多年的大规模系统迭代,长期积劳成疾导致的,所以我们需要分析找到问题的症结所在,并根据问题一一分解瓶颈的优先级。以项目为例,大致分为几个方面:1、资源包太大。通过ChromeDevTools的Network选项卡,我们可以得到页面实际拉取资源的大小(如下图所示):经过前端的快速发展,近几年项目不断更新迭代.前端建设产品也在快速增长。由于业务要放在第一位,很多同学在引入库和编码过程时没有考虑性能问题,导致构建包增大到几十MB,这带来了两个显着的问题:weak(common)网络下,耗时长在第一个屏幕上下载资源。资源解压解析速度慢。对于第一个问题,基本上会影响所有的移动用户,消耗大量不必要的用户带宽。对客户来说是一种隐性的经济损失和体验损失。对于第二个问题,会影响到所有用户,用户可能会因为等待时间过长而放弃。下图显示了延迟和用户响应:2.代码在代码执行层面耗时较长。项目迭代带来的性能问题,一般是开发者编码质量问题造成的。大概有以下几个原因:不必要的数据流监听这种场景在hooks+redux场景中会更容易出现,代码如下:constFooComponent=()=>{constdata=useSelector(state=>state.fullData);返回;};假设fullData是一个经常变化的大对象。尽管FooComponent仅依赖其.bar.baz属性,但fullData的每次更改也会导致Foo重新渲染。双刃剑cloneDeep相信很多同学都或多或少有过cloneDeep的项目经验,尤其是迭代多年的项目,难免有可变的数据处理逻辑或者业务级依赖,需要cloneDeep,但是这个方法本身存在很大的性能陷阱,如下://a.tsxexportconsta={name:'a',};//b.tsximport{a}=b;saveData(_.cloneDeep(a));//假设克隆后后端数据库上面的代码正常迭代没有问题,但是假设有一天A需要扩展一个属性保存ReactNode的引用,那么当执行到b.tsx时,浏览器可能会直接崩溃!HooksPublishing的Memohooks为React开发带来了更高的自由度,但也带来了容易被忽视的质量问题。由于不再有类中明确标注的生命周期概念,组件的状态需要开发者自由控制,所以在开发过程中一定要了解react对hooks组件的渲染机制。可以优化如下代码:constFoo=()=>{//1.Foo可以使用React.memo避免在没有props变化时渲染constresult=calc();//2.组件中不能使用直接执行的逻辑,需要用useEffect等进行封装。在数据流转的过程中,很大程度上我们会依赖lodash/fp函数来实现不可变的改变,但是fp.defaultsDeep系列函数有一个缺点。其实现逻辑相当于对原始对象进行深度克隆后执行fp.set,这可能会带来一些性能问题,并导致原始对象的所有层级属性发生变化,如下:consta={b:{c:{d:123},c2:{d2:321}}};constmerged=fp.defaultsDeep({b:{c3:3}},a);console.log(merged.b.c===a.b.c);//打印false3排查路径对于这些问题的来源,我们可以通过ChromeDevTools的Performanceflamegraph清楚地了解整个页面加载和渲染过程中每个链接的耗时和卡点(如下图):我们锁定一个需要很长时间的链接,然后我们可以通过矩阵深入树形图(下图),找到特定的耗时函数。当然,通常我们不会直接找一个耗时非常长的单点函数,而是基本上每N毫秒的函数叠加执行几百次。千次导致卡顿。所以这个profile结合react调试插件可以帮助定位渲染问题:如图所示,react组件的渲染次数和渲染时间一目了然。2如何解决性能问题?1、资源包分析作为一个有性能感的开发者,需要对自己构建的产品的内容保持敏感。这里我们使用webpack提供的stats进行产品分析。先执行webpack--profile--json>./build/stats.json获取webpack的包依赖分析数据,然后使用webpack-bundle-analyzer./build/stats.json在浏览器中看到一个buildprofile如图(不同项目的产品不同,下图只是举例):当然还有一个直观的方式,可以使用Chrome的Coverage功能帮助判断使用了哪些代码(如下图):红色表示认为没有被执行的代码是最好的构建方法一般来说,我们组织构建包的基本思路是:根据入口入口构建。一个或多个共享包供多个条目使用。基于复杂业务场景的思路是:轻量级入口。共享代码以块的形式自动生成,并建立依赖关系。大型资源包的动态导入(异步导入)。webpack4中提供了一个新的插件splitChunks来解决代码分离优化的问题。它的默认配置如下:module.exports={//...optimization:{splitChunks:{chunks:'async',minSize:20000,minRemainingSize:0,maxSize:0,minChunks:1,maxAsyncRequests:30,maxInitialRequests:30,automaticNameDelimiter:'~',enforceSizeThreshold:50000,cacheGroups:{defaultVendors:{test:/[\\/]node_modules[\\/]/,priority:-10},default:{minChunks:2,priority:-20,reuseExistingChunk:true}}}}};根据上面的配置,分块的依据是:模块是共享的,或者模块来自于node_modules。块必须大于20kb。同时并行加载的chunk或者initialpackage的个数不能超过30个。理论上webpack默认的代码分离配置是最好的方式,但是如果项目比较复杂或者耦合度比较深,我们还是需要根据构建产品大图的实际情况调整我们的chunksplit配置。解决TreeShaking失败“你项目中超过60%的代码没有被使用!”treeshaking的初衷是为了解决上面这句话中的问题,去掉不用的代码。在webpack的默认生产模式下,启用了treeshaking。通过上述构建配置,理论上应该达到“不用的代码不要打包进包”的效果,但实际情况是“你认为不用的代码,都打包进包里”Initialpackage”,这个问题通常出现在复杂的项目中,原因是代码副作用(codeeffects)。由于webpack无法判断某些代码是否“需要产生副作用”,它会将这样的代码放入包中(如下图):因此,你需要清楚地知道你的代码是否有副作用,并通过这句话来判断:“关于“副作用”被定义为在导入时执行特殊行为(修改全局对象、立即执行代码等)的代码,而不仅仅是暴露一个导出或多个导出。示例包括影响全局范围的polyfills,而且通常不提供出口。”对此,解决办法是告诉webpack我的代码没有副作用,不引入可以直接去掉。告诉的方式是:在package.json中将sideEffects标记为false。或者在webpack配置的module.rules中添加sideEffects过滤器。模块规范因此,为了在构建产品时达到最佳效果,我们在编码过程中约定了以下模块规范:【必须】模块必须是es6模块(即export和import)。【必填】超过400KB的第三方包或数据文件(如地图数据、demo数据)必须按需动态加载(异步导入)。[禁止]禁止使用export*as输出(可能导致tree-shaking失败且难以追踪)。[建议]尽量导入包中的特定文件,避免直接引入整个包(如:import{Toolbar}from'@alife/foo/bar')。【必填】依赖的第三方包必须在package.json中标记为sideEffects:false(或者在webpack配置中标记)。2Mutabledata基本上使用了Performance和React插件提供的调试能力,我们基本可以定位到问题所在。但是对于mutable数据的变化,我也会结合实践给出一些不规范的调试方法:冻结定位方法众所周知,数据流的思想产生的原因之一就是为了避免mutable数据不能的问题被追溯(因为你无法知道是哪段代码更改了数据),可变数据更改在很多项目中是无法避免的。该方法是解决可变数据变化难题的方法。这里我暂时命名为“冻结定位法”,因为其原理是使用冻结的方式来定位mutablechange问??题,比较取巧:constobj={prop:42};Object.freeze(obj);obj.prop=33;//ThrowsanerrorinstrictmodeMutabletraceback这个方法也是为了解决mutable变化带来的数据不确定性变化的问题。它用于故障排除的多个目的:在哪里读取属性。更改属性的位置。属性对应的访问链接是什么。对于下面的例子,对于一个对象的深入变化或访问,使用watchObject后,无论在哪一层设置其属性,都可以输出变化相关的信息(栈内容、变化内容等):consta={b:{c:{d:123}}};watchObject(a);constc=a.b.c;c.d=0;//打印:修改:"a.b.c.d"watchObject的原理是对一个对象进行深度封装Proxy,从而拦截get/set权限,详细可以参考:https://gist.github.com/wilsoncook/68d0b540a0fea24495d83fc284da9f4b,所以在编码过程中应该尽量避免可变数据,或者从设计上将两者分开(不同的store),否则会出现不可预知的问题,调试起来会很困难3Computing&renderingminimizesdatadependency在项目组件爆发式增长的情况下,数据flowstorecontent层级也在逐渐深入,很多组件都是依赖某个属性来触发渲染的。这个依赖需要设计的尽可能的小,避免上面提到的依赖大属性导致的频繁渲染。缓存的合理使用(1)计算结果在一些必要的CPU密集型计算逻辑中,需要使用WeakMap等缓存机制来存储当前计算的最终状态结果或中间状态。(2)组件状态对于hooks这样的组件,需要遵循以下两个原则:尽可能Memo耗时逻辑。没有多余的备忘录依赖项。Avoidcpu-intensivefunctions一些工具函数,其复杂度随着输入参数的大小而增加,还有一些会消耗大量的cpu时间。对于这类工具,尽量避免使用。如果不可避免,也可以通过“控制输入内容(白名单)”和“异步线程(webworker等)”来严格控制。比如_.cloneDeep,如果无法避免,就必须控制其参数属性中不能有引用等大数据。另外,对于上面介绍的不可变数据深度合并问题,你也应该尽量控制输入参数,或者可以参考自研的不可变实现:https://gist.github.com/wilsoncook/fcc830e5fa87afbf876696bf7a7f6bb1consta={b:{c:{d:123},c2:{d2:321}}};constmerged=immutableDefaultsDeep(a,{b:{c3:3}});console.log(merged===a);//printfalseconsole.log(merged.b.c===a.b.c);//在末尾上面打印三遍true,总结了QuickBI性能优化过程中的部分心得和体会,性能是每个开发者都绕不开的话题绕过,我们的每一段代码都标志着产品的健康。