如何在JavaScript中避免DOM阻塞
JavaScript 程序在浏览器中的单个线程和 Node.js 等运行时运行。当代码在浏览器选项卡中执行时,其他一切都会停止:菜单命令,下载,渲染,DOM 更新甚至 GIF 动画。
这对用户来说很少显而易见,因为处理在小块中很快发生。例如:单击一个按钮,该按钮会引发一个事件,该事件运行一个函数,该函数进行计算并更新 DOM。完成后,浏览器可以自由处理处理队列中的下一个项目。
JavaScript 代码不能等待某些事情发生;想象一下,如果一个应用程序每次发出 Ajax 请求时都会冻结,就会感到沮丧。因此,JavaScript 代码使用事件和回调进行操作:指示浏览器或操作系统级进程在操作完成且结果准备就绪时调用特定函数。
在以下示例中,当发生按钮单击事件时,将执行处理函数,该事件通过应用 CSS 类来激活元素。该动画完成后,匿名回调将删除该类:
//单击按钮时引发事件 document.getElementById('clickme').addEventListener('click', handleClick); // 处理按钮单击事件 function handleClick(e) { // 让元素变成动画 let sprite = document.getElementById('sprite'); if (!sprite) return; // 当动画结束时删除“animate”类 sprite.addEventListener('animationend', () => { sprite.classList.remove('animate'); }); // 添加 animate 类 sprite.classList.add('animate'); }
提供 ES2015 Promises 和 ES2017 引入 async/ await 使编码更容易,但回调仍然在表面之下使用。
Blocking Bandits
不幸的是,一些 JavaScript 操作总是同步的,包括:
- 运行计算
- 更新 DOM
- 使用 localStorage 或 IndexedDB 存储和检索数据。
下面的例子显示了一个入侵者,它使用 CSS 动画和 JavaScript 动画的组合来移动肢体。右边的图片是一个基本的 GIF 动画。按下写入按钮,默认 100,000 个会话存储操作:
感兴趣的可以到这里查看:https://codepen.io/SitePoint/pen/GzLPJV
DOM 更新在此操作期间被阻塞。入侵者会在大多数浏览器中停止或结巴。动画 GIF 动画会暂停一些。速度较慢的设备可能会显示“脚本无响应”警告。
这是一个复杂的示例,但它演示了基本操作如何影响前端性能。
Web Workers
长时间运行流程的一个解决方案是 web workers。这允许主浏览器应用程序启动后台脚本并使用消息事件进行通信。例如:
// main.js // 是否支持 web workers? if (!window.Worker) return; // 启动 web 工作者脚本 let myWorker = new Worker('myworker.js'); // 收到来自 myWorker 的消息 myWorker.onmessage = e => { console.log('myworker sent:', e.data); } // 发送消息 myWorker myWorker.postMessage('hello');
web worker 脚本:
// myworker.js // 当收到消息时启动 onmessage = e => { console.log('myworker received:', e.data); // ... 长时间运行的流程 ... // 发布消息回来 postMessage('result'); };
一个 worker 甚至可以派生其他 worker 来模拟复杂的、类似线程的操作。然而,workers 被故意限制,并且 workers 不能直接访问 DOM 或 localStorage(这样做将有效地使 JavaScript 成为多线程的,并破坏浏览器的稳定性)。因此,所有消息都以字符串的形式发送,这允许传递 json 编码的对象,但不传递 DOM 节点。
Workers 可以访问一些 window 属性、web sockets 和 IndexDB——但是他们不会改进上面的示例。在大多数情况下,工作人员被用于长时间运行的计算——比如光线跟踪、图像处理、比特币挖掘等等。
(Node.js 提供了类似于 web worker 的子进程,但是可以选择运行用其他语言编写的可执行程序。)
硬件加速动画
大多数现代浏览器不会阻止运行在它们自己的层中的硬件加速的 CSS 动画。
默认情况下,上面的示例通过更改 left-margin 移动入侵者。这一属性以及类似的属性(如 left 和 width)会导致浏览器在每个动画步骤中重绘整个文档。
使用 transform 和/或 opacity 属性时,动画更有效。这些有效地把元素放到一个单独的合成层,这样它就可以被 GPU 独立地动画。
单击硬件加速复选框,动画将立即变得更流畅。现在尝试另一个会话存储写;入侵者将继续移动,即使动画 GIF 停止。注意肢体的移动仍然会暂停,因为这是由 JavaScript 控制的。
内存中的存储
更新内存中的对象要比使用写入磁盘的存储机制快得多。在上面的例子中选择对象存储类型,然后单击 write。结果会有所不同,但是它应该比等价的会话存储操作快 10 倍左右。
内存是易失的:关闭选项卡或导航会导致所有数据丢失。一个很好的折衷方法是使用内存对象来提高性能,然后在方便的时候永久存储数据——比如当页面卸载时:
// 早先保存数据 var store = JSON.parse(localStorage.getItem('store')); // 初始化一个空存储 if (!store || !store.initialized) { store = { initialized: true, username: 'anonymous' score: 0, best: { score: 1000, username: 'Alice' } } }; // 保存到卸载页面时的本地存储 window.addEventListener('unload', () => { localStorage.setItem('store', JSON.stringify(store)); });
游戏或单页应用程序可能需要更复杂的选项。例如,当:
- 有几秒钟没有用户活动(鼠标、触摸或键盘事件)
- 游戏暂停或应用程序选项卡在后台
- 有一个自然的暂停——比如当玩家死亡,完成一个关卡,在主屏幕之间移动等等。
网络性能
Web 性能是一个热门话题。开发人员不太受浏览器限制的限制,而用户期望快速的、类似操作系统的应用程序性能。
尽可能不频繁地进行少量处理,DOM 就不会被明显阻塞。幸运的是,在无法避免长时间运行的任务的情况下,有一些选项。
用户和客户可能永远不会注意到您的速度优化,但是当应用程序变慢时,他们总是会抱怨!
码云笔记 » 如何在JavaScript中避免DOM阻塞