前端性能优化

面向的场景

  • 底层库的研发:
    • Vue、Angular UI 框架
    • Element Plus 、ant-design UI 库
    • lodash 底层工具库
  • 对性能较高的场景:
    • 大数据展示
    • 图像、影音处理

这里介绍的大部分方法,并不适合研发大部分的业务功能。

一次性能优化的流程

image.png

如何获取性能数据

  • 开发环境:通过 Devtools 等工具获取数据。
  • 生产环境:通过 Performance 等性能对象,获取数据埋点上报。

开发环境

即实验室环境,这个环节一般使用 Devtools 等开发者工具,直接从浏览器、puppeteer、Chrome 插件等,获取性能相关数据。

Devtools 获取性能数据

Chrome 的 Devtools 是最重要的性能数据获取工具,它提供了:

  • runtime performance

  • performance insights

  • memory

  • network

生产环境

因为开发环境和生产环境存在差异,所以开发环境获取的性能表现,不一定和生产环境一致,可能会出现开发环境未暴露的性能问题。 生产环境因为往往无法让开发者访问,所以无法直接获取性能数据,需要当前运行时支持的性能相关 API,如:performance、Date.now、requestAnimationFrame、requestIdleCallback 等等,间接获取性能数据。

image.png

生产环境的性能数据获取,因为生成环境通常是监控是否有性能问题,而不是要具体定位性能问题在哪,所以一般不对微观数据做获取,通常仅在宏观层面上获取性能数据即可。

性能优化方法

有了性能数据之后,就可以对当前运行时的存在的性能问题,做分析归类,一般问题有:

  • 调用时序、流程、架构:
    • 很多性能问题都是整体 调用时序、流程、架构 不合理导致的,要调整 整体时序、流程 、架构 往往风险大、成本高。
    • 对于已经成熟、稳定的系统,如果有其他微观优化空间,即使系统的整体 时序、流程、架构 是主要性能瓶颈,但是还是应该先从微观的性能瓶颈入手优化,避免宏观较大重构带来的巨大的风险与成本。
  • CPU 资源不足:因为 JS 是一个单线程语言,且渲染进程会阻塞 JS 线程,所以很容易出现 JS 内存不足的情况。如果 CPU 是性能瓶颈,往往使用如下方案:
    • 增加 CPU 核数参与计算。
    • 面向 JS 引擎原理优化。
    • 算法优化。
    • Webassembly。
  • IO 资源不足:JS 一般不会直接处理 IO,而是讲给 C++ 开发的底层模块处理 IO,所以优化方案:
    • 资源优化,如对 css、js 压缩。
    • 协议优化,如采用 HTTP2。
    • 磁盘、数据库访问策略优化。

需要根据具体的 case,采用具体优化方案,对上述问题做优化。具体优化方案、策略非常多,下面找几个方向,介绍一些优化方案。

面向 V8 的 CPU 优化方案

V8 引擎是最常用的 JavaScript 引擎,除了在 IOS 设备外(因为 IOS 上 V8 无法开启 JIT 等优化技术),几乎我们的 JavaScript 代码都是运行在 V8 引擎上面的。

另外在移动端开发中,IOS 的设备性能一般都比安卓要好,所以通常性能优化以优化安卓性能为主。

JIT 优化 (即时编译优化)

V8 中会将一部分经常使用的代码,编译成机械码,以达到增加性能的目的,我们称这种优化为 JIT 优化(即时编译优化)。 代码的写法直接决定能否被 JIT 优化,一般涉及到动态的代码,较难被 JIT 优化。

隐藏类

动态类型语言来说,由于类型的不确定性,每次访问属性时比较耗时。V8 引擎对于部分对象,会生成一个固定结构,增加访问属性速度。这个结构我们称为隐藏类。

如果该结构被破坏,V8 会花费额外的时间重新生成一个隐藏类。即牺牲对类型结构的修改,增加对类型访问的速度。

一般来说,我们优先使用类、字面量,让 V8 尽快确定类型的结构。并给每个字段提供默认值,不对属性做增、删等操作,不修改结构。 当属性不固定的场景,优先使用 Map(Hash Map),以达到提升修改对象结构效率。

隐藏类也是 JIT 优化的一种。

数组代替 Object

Object 有降级为 HashMap 的风险,而是就算是用的是隐藏类结构,考虑到原型等情况,Object 还是不如数组的访问效率。所以使用 Array 代替 Object 实现对象,可以使属性访问效率提升。尤其是访问的属性处于 miss 状态的时候,这样可以阻止在原型链上查找。

另外数组因为节省了 key,比 Object 的代码尺寸更小,利于对代码产物的大小优化。

Angular、sveltejs 等框架,都采用了这种优化方法。

修改原型代替类继承

类继承会增加原型链长度,不利于属性查找。相比类继承,直接扩展原型性能更好,对于底层面向对象开发时候,可以用直接对原型修改,代替类继承。

GC 优化

JS 引擎在做 GC 运算时候,会计算每一个对象,与 window 等全局对象的引用关系。如果某一组对象相互引用,但是最终不与 window 对象引用,这组对象就会销毁。

如果在 GC 运算前,我们将一个对象的引用关系提前解除,这样 GC 查找引用的计算量将减少,增加 GC 效率。为此我们需要给复杂对象增加一个“析构函数”,在里面解除该对象对其他对象的引用,最终减少 GC 时的计算量,提升 GC 速度。

倒排遍历

当数组数量很长的时候,采用倒序遍历 for 循环的方式效率更高。

较小的正数取整法

通过位运算符 ~ 代替 Math.floor 向下取整,性能更好。

JS 的浮点数采用的是 IEEE754 标准,有 64 位;但是 ECMAscript 的位运算只有 32 位生效(这和计算机的位长无关,就是这么要求的),该过程叫做 ToInt32,所以按位取反过程中,浮动数的小数部分会被省了掉。

但是这样做有适应性约束,因为 Int32 一共只有 32 位能用,且有一位是符号位,猜测数字最大不能超过 2 的 31 次方(I32 类型),否则会丢失精度;并且必须是正数,对负数取整,会导致与 Math.floor 的结果不同。

数字代替 boolean 类型

用位运算代替 boolean 类型,进行 true、false 运算更快。因为 JS 的位运算,支持 32 位整数实现与、或、非运算,所以一个数字,相当于 32 个 boolean 类型。

因为位运算的查询效率比 boolean 更高,所以性能更好,而且节省 key,构建出的产物也更小。 Angular、sveltejs 等框架,都采用了这种优化方法。

不要改变数组的长度

V8 引擎对初始化的数组应该是用连线表存储的,而对于修改过长度的数组,应该是用链表存储的,所以尽量不要修改数组长度。

降频、增核

节流、防抖

建议根据根据性能数据,决定节流、防抖时间等参数。否则用不好性能非但不会提升,反而会下降。

分段计算

一些耗时计算会占用 JS 的线程,导致页面卡死。此时可以将这个大运算拆分出若干下计算,放到宏任务中,这样虽然该次计算时间会变长,但是可以将 CPU 分片,交给其他任务(如渲染任务)使用,用户体验会更好。

如使用 requestIdleCallback 函数,将耗时计算延迟到下一次 CPU 空闲时间。

合并任务

事件和 setTimeout 都是宏任务,promise 是微任务,每一次执行都会向 JS 的事件循环的队列里插入任务。如果一个事件被多个地方监听,建议仅监听一次,即只向事件循环任务队列里注册一个事件。 例如用事件代理、或者复用原始事件绑定等方法。

事件代理

很简单,用事件冒泡机制,通过一个事件绑定,代替给多个 DOM 节点绑定事件。

复用事件绑定

用一个纯 JS 的事件注册、分发机制,代替原始事件,并对注册的事件计数。只有当计数从 0 到 1 的时候,绑定原生事件(一次事件分发,仅创建一个宏任务);当计数从 1 到 0 的时候,解除事件绑定。这样可以复用了宏任务,可以减少宏任务的创建。

image.png

底层库内部不用 promise,而是直接在当前的宏任务中运行 callback,可以减少微任务的创建次数。

使用 Observer

浏览器中有:IntersectionObserver、MutationObserver、ResizeObserver 3种 Observer,对检查特定事件非常方便。

原文:https://juejin.cn/post/7382765118440013875

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值