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

那些年,我们一起做的性能优化

时间:2023-03-22 11:51:24 科技观察

一直是技术层面绕不开的话题,尤其是在中大型复杂项目中。就像整车的性能一样,在追求极速的同时,也要保证舒适性和实用性。在汽车制造、零部件集成、发动机调校等每一个环节,最终都会影响到用户的体感和商业成就感,如下图PerformanceImpactonEarnings。性能优化是一个系统的、整体的事情,铭刻在项目开发过程的每一个细节中,也是体现技术深度的大战场。接下来,我将以QuickBI复杂的系统为背景,深入挖掘性能优化的思路和手段,以及系统化的思考。如何定位性能问题?一般来说,我们对动画的帧率(16ms以内)比较敏感,但是如果出现性能问题,我们的实际体感可能就一个字:“慢”,但这并不能给我们提供任何帮助在解决问题中。因此,我们需要分析这个词背后的整个链接。上图展示了浏览器的大致处理流程。结合我们的场景,我把它抽象成以下几个步骤:可以看出主要耗时的阶段分为两个:stage1:资源包下载(DownloadCode)stage2:ScriptExecution&FetchData(ScriptExecution&FetchData)如何深入到这两个阶段,我们一般使用以下主要工具来分析:Network我们首先要使用的工具是Chrome的Network,它可以帮助我们入门定位瓶颈所在的链接:As如图所示,在Network中可以一目了然整个页面:加载时间(Finish)、加载资源大小、请求次数、每个请求的耗时耗时点、资源优先级等。来自上面的例子,可以很明显的看出整个页面加载的资源非常大,接近30MB。覆盖率(codecoverage)对于复杂的前端项目,其工程建设的产品一般都是冗余的,甚至是无人使用的。通过Coverage工具可以实时分析这些无效加载的代码:如上例所示:整个页面28.3MB,其中19.5MB没有使用(执行),引擎的使用率-style.css文件小于0.7%。刚才的大资源图我们知道前端资源的利用率很低,那么无效的有哪些呢?代码是导入的?这时候我们就需要使用webpack-bundle-analyzer来分析整个构建的产品(通过webpack--profile--json=stats.json可以输出产品的stats):如上例,结合我们当前业务上,我们可以看到构建产品的问题:一是初始包过大(common.js)。二是存在多个重复包(momentjs等)。第三,依赖的第三方包太大。可以优化,但是在一个系统中,一般几百个模块相互参照组织在一起,打包工具通过依赖将它们构建在一起(比如单个common.js文件),直接去掉一个可能不太容易某些模块代码或依赖,所以我们可能需要解开到一定程度,使用工具梳理系统中模块的依赖关系,然后通过调整依赖关系或加载方式进行优化:上图我们使用的是官方的分析工具webpack(其他工具包括:webpack-xray、Madge),只需要上传资源大图stats.json就可以得到整个依赖图。资源加载相关的工具,那么在分析我们在“execution&fetching”环节中使用的是什么,Chrome提供了一个非常强大的工具:Performance:如上例所示,我们至少可以发现几点:主流程、长任务、高频任务。如何优化性能?结合刚才提到的分析工具,我们已经基本涵盖了刚刚提到的“资源包下载”和“执行&抓取”两大阶段,在不断的分析思路中也逐渐找到了根本问题和解决思路,这里我这里结合我们的场景给出一些好的优化思路和效果。大包应该按需加载。要知道前端工程构建和打包(如webpack)一般都是从入口开始寻找整个依赖树(直接Dependency),从而根据这棵树产生多个js和css文件bundle或trunk,以及一旦一个模块出现在依赖树中,当页面加载条目时,该模块也会同时加载。所以我们的想法是打破这种直接依赖,对终端模块采用异步依赖的方式,如下:会形成chunk,只在执行这段代码时(按需)加载thunk,从而减少首屏包的大小。但是,上面的方案会有问题。构建会使用整个@antv/l7作为一个chunk,而不是Marker代码的一部分,这会导致chunk的TreeShaking失败,体积会非常大。我们可以使用构建分片的方法来解决:同上,先创建Marker分片文件,使其具备TreeShaking的能力,然后在此基础上异步引入。下面是我们优化流程的对比结果:在这一步中,我们通过按需解包和异步加载的方式,节省了资源下载时间和一些执行时间。资源预加载其实我们在分析阶段就发现了一个“主进程序列化”的问题,就是js的执行是单线程的,但是浏览器其实是多线程的,其中包括异步请求(fetch等),所以我们进一步的想法是通过多线程并行的方式来获取数据(FetchData)和下载资源。根据目前的现状,接口抓取的逻辑一般耦合在业务逻辑或数据处理逻辑中,因此解耦(与UI、业务模块等逻辑解耦)被剥离出来,放在更高优先级的阶段启动要求。那么放在哪里呢?我们知道浏览器对资源的处理是有优先级的,一般是按照以下顺序:HTML/CSS/FONTPreload/SCRIPT/XHRImage/Audio/VideoPrefetch数被推进到第一优先级(HTML解析后立即执行,而不是等待SCRIPT标签资源来加载并执行请求),我们的流程就会变成如下:需要特别注意一点:由于JS的执行是串行的,所以发起fetching的逻辑必须先于main的逻辑执行进程,不能放在nextTick中(比如使用setTimeout(()=>doFetch())),否则主进程会一直占用CPU时间,导致请求无法发出活动任务调度浏览器也有优先策略对于资源,但是它不知道我们要在业务层面先加载/执行哪些资源,哪些资源要后加载/执行,所以我们跳出来看看,如果整个业务层面的资源是loading+execution/retrieval过程分成小任务。这些任务完全由我们自己控制:打包粒度、加载时机、执行时机。这是否意味着可以最大化CPU时间和网络资源?答案是肯定的,但一般对于简单的项目,浏览器本身的调度优先级策略就足以满足需求,但是如果要对大型复杂的项目做比较极端的优化,则需要引入“自定义任务调度”“计划。以QuickBI为例,我们早期的目标是让首屏主要内容显示的更快。然后,CPU/网络分配应根据我们在资源加载、代码执行和数据检索方面的业务优先级进行分配。比如我希望“卡片下拉菜单”只在首屏主要内容显示完或者CPU空闲的时候才显示。开始加载(即降低优先级,甚至当用户将鼠标悬停在卡片上时,您希望它提高优先级并立即开始加载和显示)。如下:这里我们封装了一个任务调度器,它的作用是声明一段逻辑,在它的一个依赖(Promise)完成后开始执行。我们的流程图变化如下:黄色块代表一些模块,用于优先级降级处理,有助于减少整个首屏时间。TreeShaking的上述方法大多是从优先级开始的。事实上,在前端工程越来越复杂的时代(中大型项目已经超过了几十万行代码),一种更智能的优化方案诞生了,以减少包的大小。这个想法很简单:基于工具的依赖性分析和从最终产品中删除未引用的代码。听起来很酷,在实践中用起来也很不错,不过这里想说说它的官网很多都不会提到的一些点---TreeShaking经常失败:副作用(SideEffects)通常表示全局(比如窗口对象等)或受环境影响的代码。如图,b代码好像没用,但是它的文件里有console.log(b(1))这样的代码,webpack等打包工具不敢轻易去掉,所以会进入照常。解决方法是在package.json或者webpack的配置中明确说明哪些代码有副作用(比如sideEffects:[“**/*.css”]),没有副作用的代码会被去掉。IIFE类代码IIFE会立即执行函数表达式(Immediatelyinvokedfunctionexpression)如图,这类代码会导致TreeShaking失败解决的三个原则:【避免】立即执行函数调用【避免】立即执行新操作【避免】立即影响全局代码的Lazyloading我们在“按需加载”中提到,解包的异步导入会导致TreeShaking失败。这里还有一个案例进一步说明一下:如图,因为index.ts在bar.ts中同步import了sharedStr,然后在某处,同时异步import('./bar'),在这种情况下,它会同时导致两个问题:TreeShaking失败(unusedStr会被类型化)异步延迟加载失败(bar.ts会和index.ts合二为一)当代码量达到一定程度时,很容易N个人来开发这个问题。解决方案【避免】同一个文件的同步和异步导入按需策略(Lazy)其实就是上面提到的一些按需加载的方案,这里适当扩展一下:既然可以做资源包的加载ondemand,某个组件的渲染可以按需完成吗?对象实例可以按需使用吗?能否按需生成数据缓存?惰性组件(LazyComponent)如图所示。PieArc.private.ts对应一个复杂的React组件。PieArc通过makeLazyComponent封装成一个默认的懒加载组件。只有执行到这里的代码,组件才会被加载并执行。甚至,可以通过第二个参数(deps)来声明依赖,直到依赖(promise)完成后才会加载执行。惰性缓存(LazyCache)惰性缓存用于这种场景:你需要在任何地方使用数据流中某个数据(或其他可订阅数据)的转换结果,并且只在使用的那一刻进行转换。对象(LazyObject)惰性对象是指对象只有在被使用时(属性/方法被访问、修改、删除等)才会被实例化。调用globalRecorder.record()时,实例化数据流:为了便于状态大型项目在节流渲染管理中,通常采用数据流方案,如下:store中存储的数据通常是原子的,粒度很小,比如state中有N个原子属性:a、b、c……等等。一个组件依赖于这N个属性来进行UI渲染。假设在不同的ACTION下会发生N个属性的变化,而这些变化都发生在16ms以内,所以如果N=20,那么16ms内会有20次View更新(1帧):这显然会造成非常大的性能问题,所以我们需要对短期的ACTION量做一个缓冲Throttling,20次ACTION状态变化后,只进行1次View更新,如下:该方案在QuickBI中采用redux中间件的形式工作,效果不错在复杂+频繁的数据更新场景。想着烦恼,预防着”,回过头来看,这些性能问题80%以上都可以在架构设计和编码阶段避免,20%可以用“空间<=>时间”的策略来代替”和其他方式来平衡。因此,最好的性能优化解决方案在于我们对每一段代码质量的专注:我们是否考虑过这种模块依赖可能导致的构建产品的大小?是否考虑过可能的执行这个逻辑的频率?你有没有考虑到数据增长时空间或CPU使用率的可控性?等等。性能优化没有灵丹妙药。作为技术人员,你需要修炼自己(熟悉底层原理)和将你对性能的痴迷植入你的本能思维,这就是灵丹妙药。原文链接:http://click.aliyun.com/m/1000283335/