前文中已经介绍了几种性能优化的方式,接下来继续介绍以下几种:
用 Event 代替 setTimeout 创建宏任务
setTimeout 是最简单的创建宏任务的方法,所以很多场景喜欢使用该 API 创建宏任务。不过 setTimeout 存在延时,它创建的宏任务往往不会立刻执行,中间可能会插入其他宏任务。 所以推荐采用创建 Event 回调的方式创建宏任务,例如 Vue 中 NextTick 函数,采用 MessageChannel 和 message 回调创建宏任务。
增加 Worker
Worker / SharedWorker
增加 JS 并行数,增加参与运算的 CUP 数量。
跨域 JS 运行时和 noopener
同域的 iframe 或 open 打开的页面,是默认与当前页面共用一个线程的。这是因为这些页面可以通过 opener、top 访问到当前页面的全局对象。出于数据安全访问的目的,必须禁止并行对数据修改,所以这种场景会强制要求 JS 运行时复用一个线程。 如果不需要彼此访问数据,可以用跨域的 iframe 或者 open + noopener, 这样做各个页面的 JS 运行时之间不再共用一个线程,相当于用了 worker。
数据跨 JS 运行环境拷贝
通过增加 Worker、SharedWork,将更多 CPU 用于计算。因为 JS 是单线程语言,没有线程锁的概念,跨线程之间的数据传递会先做深拷贝,以便跨 Worker 的修改数据行为是安全的。但是对于大数据,计算收益需要将它的拷贝时间也考虑进去,所以不要盲目地增加线程。 如果要优化大文件的传输时间,一个是如果要在多个异步引擎间传递数据,要用 MessageChannel,减少传递次数,另一个是用 postMessage 的所有权转换。 还有一种方式是用 SharedArrayBuffer,这样不需要拷贝数据,直接让多个线程共享数据,不过类似其他语言一样,需要将同步锁。 实际测试发现都快不了多数,总之尽可能减少跨线程传递大文件的事。
算法优化
优化方法太多了,简单介绍几个常用的:
Hash 表代替数组(建立索引)
因为 Hash 表查询的时间复杂度是 O(1),比在数组中随机查询要快。例如我们要频繁在一组数据中,根据 ID 找到指定数据,可以先把这些输入放到 Map 中,并用 ID 做 key,再通过 ID 查询数据。
查找表
如果计算过程的结果,只有输入决定,且输入是可枚举的,则可以把所有计算结果事先计算出来,然后使用根据输入直接取输出。
缓存
缓存可以减少重复的计算,复用前面计算结果,使用合理的话可以取得不存的优化效果。 缓存的方式也是多种多样,不过一般建议采用 函数式编程思想,实现缓存结果、判断缓存命中、缓存淘汰。
基于函数式思想做缓存
因为纯函数要求:输出仅由输入决定,所以可以 通过输入,缓存对应的输出结果。如:
- 加法函数
const add = (a, b) => a + b
- 需要缓存结果
const cache = new map();
const add = (a, b) => {
const key = a + ',' + b;
if (!cache.has(key)) {
cache.set(key, key);
}
return cache.get(key);
}
为了方便缓存结果,尽量将耗时操作改造成纯函数,这样不但好缓存,也方便测试。
缓存命中判断方法
- 根据入参是否变更 就是根缓存内容加 Key,如果需要缓存的内容,是一个纯函数的计算结果,则可以使用 入参 做缓存的 Key。方法有:
- 对 入参 求 hash 值
- 对 入参 序列化
- 要求 入参 是 不可变数据
- 对 入参 使用 diff 算法
根据计算过程是否变更
如果需要缓存的计算过程不是纯函数,那么理论上它是不能缓存结果的。但是如果某个计算过程是在一定场景下,符合纯函数条件,则可以在该条件下,进行缓存;一定条件变更,清除所有缓存。 例如:数据库的查询结果,在数据库数据未变更的情况下,我们可以将查询结果缓存起来。如果数据被修改了,则缓存失效,清除缓存。
缓存淘汰
可以根据 performance.memory 获取可用内存大小,然后创建一个指定大小的容器保存缓存,并根据合适的算法,当缓存的内容超过上限时,淘汰缓存:
- LRU:首先淘汰最长时间未被使用的内容
- FIFO:先存入缓存的内容,先被淘汰
- LFU:首先淘汰一段时间,最少被使用的内容
DOM 的节流、重绘优化
现代浏览器对 DOM 的修改都比传统 IE 时代快了不少,即使修改了 DOM,也不会立刻回流、重绘。所以即使不使用 DocumentFragment,也能获取较好的性能效果。 但是如果修改了 DOM,立刻做了布局、渲染相关的查询,就会导致强制回流、重绘,对性能造成较大的影响。
DOM 都修改完,再查询
查询会强制 节流、重绘。如果有大量的 DOM 修改操作,并且每次修后都立刻查询,就会导致大量的无效回流。为了避免无效回流,通常都是先将 DOM 修改全部改完,再查询。
使用 Vue、React 等框架,虽然性能上限不如 vanilla 开发高,但是基本都能保证去掉无效回流,要比 vanilla 开发性能下限高。所以推荐初学者使用 Vue、React 等框架开发 Web 项目。
DOM 修改尽量不要产生回流
修改 CSS 的时候,尽量用不会产生回流的修改。例如用 transform 代替绝对布局。
增加预处理
总体思路是:如果有些事情,可以在运行时之前完成,就没必要留到运行时再做。
预加载、预计算
略。
编译器扩展
通过对编译器扩展,可以通过代码静态分析,将一部分工作在运行时执行前,在编译期间完成;运行时则可以免去计算,而直接用编译好的结果。 如:
- 生成查找表
- 在编译时对 DSL 解析(例如 Vue、React 在编译期间解析模板)
Webassembly
对于 CPU 密集型场景,除了降频、增核,还可以使用 Webassembly 这种性能更高、对 CPU 利用率更高的方案。但是 Webassembly 无法修改 DOM,且和 JS 交换数据比较麻烦,因此 Webassembly 也并非银弹,具体场景具体分析。 比较推荐的做法是,尽量让 Webassembly 和 JS 共享一份数据,这样不需要频繁交互数据。(因为 Webassembly 和 JS 共用一个线程,所以不用担心数据安全问题) 或者是使用 Blazor 这种,完全由 C# 编写的框架,避免 Webassembly 和 JS 频繁交替。