关于性能优化:那些年我们一起做过的性能优化

47次阅读

共计 4803 个字符,预计需要花费 13 分钟才能阅读完成。

简介:性能优化是一个体系化、整体性的事件,印刻在我的项目开发环节的各个细节中,也是体现技术深度的大的战场。文章以 Quick BI 的简单零碎为背景,具体介绍性能优化的思路和伎俩,以及体系化的思考。

始终以来,性能都是技术层面不可避开的话题,尤其在中大型简单我的项目中。犹如汽车整车性能,谋求极速的同时,还要保障舒适性和实用性,而在汽车制作的每个环节、整机整合状况、发动机调校等等,都会最终影响用户体感以及商业达成,如下图性能对收益的影响。

性能优化是一个体系化、整体性的事件,印刻在我的项目开发环节的各个细节中,也是体现技术深度的大的战场。上面我将以 Quick BI 的简单零碎为背景,深扒整个性能优化的思路和伎俩,以及体系化的思考。

如何定位性能问题?

通常来讲,咱们对动画的帧率是比拟敏感的(16ms 内),但如果呈现性能问题,咱们的理论体感可能就一个字:“慢”,但这并不能为咱们解决问题提供任何帮忙,由此咱们须要分析这个字背地的整条链路。

上图是浏览器通用的解决流程,联合咱们的场景,我这里形象成以下几个步骤:

能够看出,次要的耗时阶段分为两个:

阶段一:资源包下载(Download Code)
阶段二:执行 & 取数(Script Execution & Fetch Data)

如何深刻这两个阶段,咱们个别会用以下几个次要的工具来剖析:

Network

首先咱们要应用的一个工具是 Chrome 的 Network,它能帮忙咱们初步定位瓶颈所在的环节:

如图示例,在 Network 中能够高深莫测看到整个页面的:加载工夫(Finish)、加载资源大小、申请数量、每个申请耗时及耗时点、资源优先级等等。下面示例能够很显著看出:整个页面加载的资源很大,靠近了 30MB。

Coverage(代码覆盖率)

对于简单的前端工程,其工程构建的产物个别会存在冗余甚至未被应用的状况,这些有效加载的代码能够通过 Coverage 工具来实时剖析:

如上图示例能够看到:整个页面 28.3MB,其中 19.5MB 都未被应用(执行),其中 engine-style.css 文件的使用率只有不到 0.7%

资源大图

方才咱们曾经晓得前端资源的利用率非常低,那么具体是哪些有效代码被引入进来了?这时候咱们要借助 webpack-bundle-analyzer 来剖析整个的构建产物(产物 stats 能够通过 webpack –profile –json=stats.json 输入):

如上例,联合咱们以后业务能够看到构建产物的问题:

第一,初始包过大(common.js)

第二,存在多个反复包(momentjs 等)

第三,依赖的第三方包体积过大

模块依赖关系

有了资源构建大图,咱们也大略晓得了可优化的点,但在一个零碎中,成千盈百的模块个别都是通过相互援用的形式组织在一起,打包工具再通过依赖关系将其构建在一起(比方打成 common.js 单个文件),想要间接移除掉某个模块代码或依赖可能并非易事,由此咱们可能须要肯定水平抽丝剥茧,借助工具理清零碎中模块的依赖关系,再通过调整依赖或加载形式来作优化:

上图咱们应用到的是 webpack 官网的 analyse 工具(其余工具还有:webpack-xray,Madge),只须要将资源大图 stats.json 上传即可失去整个依赖关系大图

Performance

后面讲到的都是和资源加载相干的工具,那么在剖析“执行 & 取数”环节咱们应用什么,Chrome 提供了十分弱小的工具:Performance:

如上图示例,咱们能够至多发现几个点:主流程串化、长工作、高频工作。

如何优化性能?

联合方才提到的剖析工具,方才提到的“资源包下载”、“执行 & 取数”两个大的阶段咱们基本上曾经笼罩到,其基本问题和解法也在一直的剖析中逐渐有了思路,这里我将联合咱们这里的场景,给出一些不错的优化思路和成果

大包按需加载

要晓得,前端工程构建打包(如 webpack)个别是从 entry 登程,去寻找整棵依赖树(间接依赖),从而依据这棵树产出多个 js 和 css 文件 bundle 或 trunk,而一个模块一旦呈现在依赖树中,那么当页面加载 entry 的时候,同时也会加载该模块。

所以咱们的思路是突破这种间接依赖,针对末端的模块改用异步依赖形式,如下:

将同步的 import {Marker} from ‘@antv/l7’ 改为异步,这样在构建时,被依赖的 Marker 会造成一个 chunk,仅在此段代码执行时(按需),该 thunk 才被加载,从而缩小了首屏包的体积。

然而下面计划会存在一个问题,构建会将整个 @antv/l7 作为一个 chunk,而非 Marker 局部代码,导致该 chunk 的 TreeShaking 生效,体积很大。咱们能够应用构建分片形式解决:

如上,先创立 Marker 的分片文件,使之具备 TreeShaking 的能力,再在此基础上作异步引入。

下方是咱们优化后的流程比照后果:

这一步,咱们通过按需拆包,异步加载,节俭了资源下载工夫和局部执行工夫

资源预加载

其实咱们在分析阶段曾经发现一个“主流程串化”的问题,js 的执行是单线程,但浏览器实际上是多线程运行的,这外面就包含异步申请(fetch 等),所以咱们进一步的思路是把取数(Fetch Data)与资源下载通过多线程并行。

依照以后现状,接口取数的逻辑个别是耦合在业务逻辑或数据处理逻辑中的,所以解耦(与 UI、业务模块等解耦)的步骤必不可少,将纯正的 fetch 申请(及大量解决逻辑)剥离进去,放到优先级更高的阶段来发动申请。那么放到什么中央呢?咱们晓得,浏览器对资源的解决是有优先级的,失常按如下程序:

  1. HTML/CSS/FONT
  2. Preload/SCRIPT/XHR
  3. Image/Audio/Video
  4. Prefetch

要做到资源拉取 和 发动取数并行,就有必要把取数提前到第 1 优先级(HTML 解析结束后立刻执行,而非期待 SCRIPT 标签资源加载执行过程中发动申请),咱们的流程会变成如下:

须要特地留神一点:因为 JS 的执行是串行,发动取数的那段逻辑必须要先于主流程逻辑执行,并且不能放到 nextTick(如应用 setTimeout(() => doFetch())),否则主流程会始终占用 CPU 工夫使得申请无奈收回

被动任务调度

浏览器对资源也有优先级策略,但它并不知道业务层面的咱们,到底想要哪些资源先加载 / 执行,哪些资源后加载 / 执行,所以咱们跳出来看,若把整个业务层面的资源加载 + 执行 / 取数流程拆成一个一个小的工作,这些工作全权由咱们本人来管制其:打包粒度、加载机会、执行机会,是不是意味着能最大化利用 CPU 工夫和网络资源了?

答案是必定的,不过个别对于简略的我的项目,浏览器自身的调度优先级策略曾经足够满足需要,但如果针对大型简单我的项目,要做的绝对极致的优化,就有必要引入“自定义任务调度”计划了。

以 Quick BI 为例,咱们的后期指标是:让首屏次要内容展示更加疾速。那么从资源加载、代码执行、取数层面是应该依据咱们业务优先级作 CPU/ 网络调配的,比方:我心愿“卡片的下拉菜单”,在首屏次要内容展现结束后或 CPU 闲暇时,才开始加载(即升高优先级,更甚至在用户鼠标移入卡片中时,又心愿它进步优先级立刻开始加载并展现)。如下:

这里咱们封装了一个任务调度器,其目标是能够申明一段逻辑,在其某个依赖(Promise)实现后开始执行。咱们的流程图变动如下:

黄色区块代表 作优先级降级解决的局部模块,其帮忙缩小了整个首屏工夫

TreeShaking

下面讲办法大多从优先级登程,其实在前端工程化日益简单的时代(中大型项目已超几十万行代码),诞生了一个较为智能的优化计划用于缩小包大小,其思维很简略:工具化剖析依赖关系,将没有被援用到的代码从最终产物中剔除掉。

听起来很酷,理论用起来也十分不错,但这里想讲一些很多其官网也不会提到的点 — TreeShaking 常常生效的状况:

副作用

副作用(Side Effects)通常表白的是对全局(如 window 对象等)或环境会产生影响的代码。

如图示例,b 代码看似未被应用,但其文件中存在 console.log(b(1)) 这样的代码,webpack 等打包工具不敢轻易移除它,所以它会被照常打入。

解决办法

在 package.json 或 webpack 配置中明确指定哪些代码具备副作用(例如 sideEffects: [“*/.css”]),无副作用的代码将被移除

IIFE 类代码

IIFE 即会被立刻执行的函数表达式(Immediately invoked function expression)

如图,这类型的代码,会导致 TreeShaking 生效

解决办法

三个准则:

• [防止] 立刻执行的函数调用

• [防止] 立刻执行的 new 操作

• [防止] 立刻影响全局的代码

懒加载

咱们在“按需加载”处提到过异步 import 来做拆包会导致 TreeShaking 生效,这里再进一步阐明一下另外一个 case:

如图,因为 index.ts 同步 import 了 bar.ts 中的 sharedStr,而后在某个中央,又同时异步 import(‘./bar’),这种状况下,会同时导致两个问题:

  1. TreeShaking 生效(unusedStr 会被打入)
  2. 异步懒加载生效(bar.ts 会和 index.ts 打入到一起)
    当代码量达到一定量级,N 集体协同开发就很容易呈现这个问题

解决办法

• [防止] 同步和异步 import 同个文件

按需策略(Lazy)

其实后面有讲到一些按需加载的计划,这里咱们适当延长一下:既然资源包的加载能够做到按需,是否某个组件的渲染能够按需?某个对象实例的应用能够按需?某个数据缓存的生成也能够按需?

懒组件(LazyComponent)

如图,PieArc.private.ts 对应一个简单的 React 组件,PieArc 通过 makeLazyComponent 封装成默认懒加载的组件,只有在代码执行到此处时,组件才会加载并执行。甚至,还能够通过第二个参数(deps)申明依赖,待依赖(promise)结束时,才加载和执行。

懒缓存(LazyCache)

懒缓存用于这种场景:须要在任何中央应用到数据流(或其余可订阅数据)中的某个数据通过转换后的后果,且仅在应用的那一刻才进行转换

懒对象(LazyObject)

懒对象意即该对象只有在被应用的时候(属性 / 办法被拜访、批改、删除等等),才会被实例化

如图,globalRecorder 被引入时,其并未实例化,仅当调用 globalRecorder.record() 时进行实例化

数据流:节流渲染

中大型项目中为了不便状态治理,通常会应用到数据流的计划,如下流程:

store 中存储的数据通常偏原子化,粒度十分小,比方 state 中有:a、b、c … 等 N 个原子属性,某个组件依赖这 N 个属性来作 UI 渲染,假如 N 个属性会在不同的 ACTION 下被扭转,且这些扭转均在 16ms 内产生,那么若 N =20,则 16ms 内(1 帧)会有 20 次 View 更新:

这显然会引发十分大的性能问题,由此,咱们须要对短时间的 ACTION 量作一个缓冲节流,待 20 次 ACTION 状态扭转结束后,仅进行 1 次 View 更新,如下:

此计划在 Quick BI 以 redux 中间件的模式发挥作用,在简单 + 频繁数据更新场景起到了不错的成果

思考

“小人以思患而豫防之”,当咱们回过头去看看,呈现的这些性能问题,在架构设计、编码阶段是能够防止掉 80% 以上的,20% 的则能够“空间 <=> 工夫置换策略”等形式去均衡。所以,最佳的性能优化计划,是在于咱们对每一段代码品质的执着:是否思考到了这样的模块依赖关系,可能带来的构建产物体积问题?是否思考到了这段逻辑可能的执行频次?是否思考到了随着数据增长,空间或 CPU 占用的可控性?等等。性能优化没有银弹,作为技术人,须要内修于心(熟知底层原理),把对性能的执念植入本能思考当中,方为银弹。

原文链接

本文为阿里云原创内容,未经容许不得转载。

正文完
 0