加载和执行
脚本位置
浏览器在执行 JavaScript 代码时,不能同时做任何事情,只能让页面等待,进行脚本的解析和执行(因为脚本执行过程中可能会修改页面内容)因此推荐将所有 <script> 标签尽可能的放到 <body> 标签的底部,尽量减少对整个页面下载的影响。将 css 文件放到 <head>中是因为css使用href异步加载,不会造成页面的阻塞,放在html头部,可以和DOM 树一同渲染,可以防止页面出现白屏,闪跳或布局混乱。
无阻塞的脚本
减小页面中外链脚本的数量可以改善性能,因为HTTP请求会带来额外的性能开销,但尽管下载单个较大文件只产生一次HTTP请求,却会锁死浏览器一大段时间,所以尽量采用逐步加载JavaScript文件的方式:
- Defer 属性:指明本元素脚本不会修改DOM,该文件将在页面解析到
<script>标签时开始下载,但并不会执行,直到DOM加载完成(onload事件被触发前)。 - 动态脚本元素:使用 js 创建 script 标签,动态添加。文件会在该元素被添加到页面时开始下载,并且不会阻塞页面其他进程。此方法跨浏览器兼容性和易用性都很好,是最通用的无阻塞加载解决方案。
- 使用XHR 对象下载 JavaScript 代码并注入页面,缺点是必须与所请求的页面处于相同的域。
数据存取
函数每次执行时对应的执行环境都是独一无二的,多次调用同一个函数会导致创建多个执行环境,函数执行完毕,执行环境就会被销毁。在函数执行过程中,每遇到一个变量都会去搜索执行环境的作用域链,直到找到标识符,这个搜索的过程影响了性能。
- 访问字面量和局部变量的速度最快,访问数组元素和对象成员相对较慢。
- 局部变量在作用域链的起始位置,全局变量在作用域链的最末端,因此访问局部变量比访问跨作用域变量更快,位置月神,时间越长,而全局变量的访问是最慢的。
- 如果某个跨作用域的值在函数中被引用一次以上,那么就把它存到局部变量里来改善性能。
DOM 编程
重绘:DOM变化影响了元素几何属性(如改变了宽高),导致浏览器需要重新计算,重新构造渲染树。重排:浏览器重新绘制受影响的部分到屏幕中。(改变颜色等只会发生重绘,不需要重排)
- 用脚本进行DOM操作的速度很慢,所以应该尽量减少访问 DOM 的次数,把运算尽量留在JavaScript端处理。(所以 React 等框架都使用了虚拟 DOM,为的也是减少性能损失 )
- 如果需要多次访问某个 DOM 节点,使用局部变量存储它的引用。
- 使用速度更快的API,如 querySelectorAll()和 firstElementChild。
- 留意重排和重绘,合并多次修改,“离线”操作 DOM 树,使用缓存,减少访问布局信息的次数(会导致强制刷新重排)。
- 动画中使用绝对定位,使用拖放处理。
- 每绑定一个事件处理器都是有代价的,要么是加重了页面负担,要么是增加运行期间的执行时间,所以尽量使用事件委托来减少事件处理器的数量(React 就将所有事件都委托到了 document 上,React 17 将事件委托的节点修改到了 root 根节点。)
算法和流程控制
- for-in 可以枚举任何对象的属性名,也因为它每次迭代都会同时搜索实例或原型属性,所以比其他循环都要慢,尽量不要使用。
- 可以通过减小每次迭代的事务,和迭代的次数来提升性能。
- 在判断条件较多时,使用查找表(
xxMap[key])比 if-else 和 switch 更快。 - 使用循环代替长时间运行的递归函数也可以提升性能,因为运行一个循环比反复调用一个函数开销要少得多。
快速响应的用户界面
JavaScript 和用户界面更新在一个进程中进行,因此一次只能处理一件事情,高效管理 UI 线程就是确保 JavaScript 不能运行太长时间,以免影响用户体验。
- 任何 JavaScript 任务都不应当执行超过 100 ms,过长的运行时间会导致明显的延迟。
- 可以用定时器将长时间运行的脚本分解成一系列小任务。
- Web Workers 允许在 UI 线程外部执行 JavaScript 代码,避免锁定 UI。