19个常见的JavaScript和Node.js错误以及优化
缓慢或卡顿的网站是业余爱好者的标志,而流畅、优化的体验会让用户感到高兴,并使专业人士脱颖而出。
但创建真正高性能的网络应用程序充满了陷阱。错误比比皆是,它们可能会拖慢JavaScript的速度,而您甚至没有意识到这一点。微小的疏忽会让你的代码变得臃肿,并悄悄地一点一点地降低速度。
这是怎么回事?
事实证明,我们有很多常见的方式会无意中减慢 JavaScript 的速度。随着时间的推移,可能会阻碍网站性能。
这些错误是可以避免的。
今天,我们重点关注可能会悄悄减慢 JavaScript 和 Node.js 应用程序速度的 19 个性能陷阱。我们将通过说明性示例和可操作的解决方案来探索导致这些问题的原因,以优化您的代码。
识别并消除这些危害是打造让用户满意的流畅网络体验的关键。那么,让我们深入了解一下吧!
1. 不正确的变量声明和作用域
第一次学习 JavaScript 时,很容易在全局声明所有变量。然而,这会导致未来出现问题。让我们看一个例子:
// globals.js
var color = 'blue';
function printColor() {
console.log(color);
}
printColor(); // Prints 'blue'
这工作正常,但想象一下如果我们加载另一个脚本:
// script2.js
var color = 'red';
printColor(); // Prints 'red'!
因为color是全局的,所以script2.js覆盖了它!要解决此问题,请尽可能在函数内部声明变量:
function printColor() {
var color = 'blue'; // local variable
console.log(color);
}
printColor(); // Prints 'blue'
现在,其他脚本的更改不会影响printColor.
不必要时在全局范围内声明变量是一种反模式。尝试将全局变量限制为配置常量。对于其他变量,请在尽可能小的范围内进行本地声明。
2. 低效的 DOM 操作
更新 DOM 元素时,批量更改而不是一次操作一个节点。考虑这个例子:
const ul = document.getElementById('list');
for (let i = 0; i < 10; i++) {
const li = document.createElement('li');
li.textContent = i;
ul.appendChild(li);
}
这将逐一附加列表项。最好先构建一个字符串然后设置.innerHTML:
const ul = document.getElementById('list');
let html = '';
for (let i = 0; i < 10; i++) {
html += `<li>${i}</li>`;
}
ul.innerHTML = html;
构建字符串可以最大限度地减少回流。我们更新 DOM 一次而不是 10 次。
对于多个更新,构建更改,然后在最后应用。或者更好的是,使用 DocumentFragment 批量追加。
3. 过多的 DOM 操作
频繁的DOM更新会降低性能。考虑一个将消息插入页面的聊天应用程序。
反面例子:
// New message received
const msg = `<div>${messageText}</div>`;
chatLog.insertAdjacentHTML('beforeend', msg);
这天真地插入到每条消息上。最好是限制更新:
正确示例:
let chatLogHTML = '';
const throttleTime = 100; // ms
// New message received
chatLogHTML += `<div>${messageText}</div>`;
// Throttle DOM updates
setTimeout(() => {
chatLog.innerHTML = chatLogHTML;
chatLogHTML = '';
}, throttleTime);
现在,我们最多每 100 毫秒更新一次,从而保持 DOM 操作较低。
对于高度动态的 UI,请考虑像 React 这样的虚拟 DOM 库。这些最大限度地减少了使用虚拟表示的 DOM 操作。
4.缺乏活动委托
将事件侦听器附加到许多元素会产生不必要的开销。考虑一个每行都有删除按钮的表:
反面例子:
const rows = document.querySelectorAll('table tr');
rows.forEach(row => {
const deleteBtn = row.querySelector('.delete');
deleteBtn.addEventListener('click', handleDelete);
});
这会为每个删除按钮添加一个侦听器。需要更好地使用事件委托:
正确示例:
const table = document.querySelector('table');
table.addEventListener('click', e => {
if (e.target.classList.contains('delete')) {
handleDelete(e);
}
});
现在,.net 上只有一个侦听器,更少的内存开销。
事件委托利用事件冒泡。一个侦听器可以处理来自多个后代的事件。只要适用,就使用委派。
5. 低效的字符串连接
在循环中连接字符串时,性能会受到影响。考虑这段代码:
let html = '';
for (let i = 0; i < 10; i++) {
html += '<div>' + i + '</div>';
}
创建新字符串需要分配内存。最好使用数组:
const parts = [];
for (let i = 0; i < 10; i++) {
parts.push('<div>', i, '</div>');
}
const html = parts.join('');
构建数组可以最大限度地减少中间字符串。.join()最后连接一次。
对于多个字符串添加,请使用数组连接。另外,请考虑嵌入值的模板文字。
6. 未优化的循环
JavaScript 中的循环经常会导致性能问题。一个常见的错误是重复访问数组长度:
反面例子:
const items = [/*...*/];
for (let i = 0; i < items.length; i++) {
// ...
}
冗余检查.length会抑制优化。
正确示例:
const items = [/*...*/];
const len = items.length;
for (let i = 0; i < len; i++) {
// ...
}
缓存长度可以提高速度。其他优化包括将不变量提升到循环之外、简化终止条件以及避免迭代内昂贵的操作。
7. 不必要的同步操作
JavaScript 的异步功能是一个关键优势。但要小心阻塞 I/O!例如:
反面例子:
const data = fs.readFileSync('file.json'); // blocks!
这会在从磁盘读取时停止执行。相反,如果使用回调或承诺:
正确示例:
fs.readFile('file.json', (err, data) => {
// ...
});
现在,事件循环在读取文件时继续。对于复杂的流程,async/await简化异步逻辑。避免同步操作以防止阻塞。
8. 阻止事件循环
JavaScript 使用单线程事件循环。阻止它会停止执行。一些常见的拦截器:
- 繁重的计算任务
- 同步输入/输出
- 未优化的算法
例如:
function countPrimes(max) {
// Unoptimized loop
for (let i = 0; i <= max; i++) {
// ...check if prime...
}
}
countPrimes(1000000); // Long running!
这会同步执行,并阻止其他事件。避免:
- 推迟不必要的工作
- 批量数据处理
- 使用工作线程
- 寻找优化机会
- 保持事件循环顺利运行。定期分析以捕获阻塞代码。
9. 错误处理效率低下
在 JavaScript 中正确处理错误至关重要。但要小心性能陷阱!
反面例子:
try {
// ...
} catch (err) {
console.error(err); // just logging
}
这会捕获错误但不采取纠正措施。未处理的错误通常会导致内存泄漏或数据损坏。
正确示例:
try {
// ...
} catch (err) {
console.error(err);
// Emit error event
emitError(err);
// Nullify variables
obj = null;
// Inform user
showErrorNotice();
}
记录还不够!清理工件、通知用户并考虑恢复选项。使用 Sentry 等工具来监控生产中的错误。明确处理所有错误。
11. 内存泄漏
当内存被分配但从未释放时,就会发生内存泄漏。随着时间的推移,泄漏会累积并降低性能。
JavaScript 中的常见来源包括:
- 未清理的事件监听器
- 对已删除 DOM 节点的过时引用
- 不再需要的缓存数据
- 闭包中的累积状态
例如:
function processData() {
const data = [];
// Use closure to accumulate data
return function() {
data.push(getData());
}
}
const processor = processData();
// Long running...keeps holding reference to growing data array!
数组不断变大,但从未被清除。修理:
- 使用弱引用
- 清理事件监听器
- 删除不再需要的引用
- 限制关闭状态大小
- 监视内存使用情况并观察增长趋势。在泄漏堆积之前主动消除泄漏。
11. 过度使用依赖项
虽然 npm 提供了无穷无尽的选择,但请抵制过度导入的冲动!每个依赖项都会增加包大小和攻击面。
反面例子:
import _ from 'lodash';
import moment from 'moment';
import validator from 'validator';
// etc...
为次要实用程序导入整个库。最好根据需要挑选助手:
正确示例:
import cloneDeep from 'lodash/cloneDeep';
import { format } from 'date-fns';
import { isEmail } from 'validator';
只导入您需要的内容。定期检查依赖关系以删除未使用的依赖关系。保持捆绑精简并最大限度地减少依赖性。
12. 缓存不足
缓存允许通过重用先前的结果来跳过昂贵的计算。但它经常被忽视。
反面例子:
function generateReport() {
// Perform expensive processing
// to generate report data...
}
generateReport(); // Computes
generateReport(); // Computes again!
由于输入没有更改,因此可以缓存报告:
正确示例:
let cachedReport;
function generateReport() {
if (cachedReport) {
return cachedReport;
}
cachedReport = // expensive processing...
return cachedReport;
}
现在,重复调用速度很快。
13. 未优化的数据库查询
与数据库交互时,低效的查询可能会降低性能。需要避免的一些问题:
反面例子:
// No indexing
db.find({name: 'John', age: 35});
// Unecessary fields
db.find({first: 'John', last:'Doe', email:'john@doe.com'}, {first: 1, last: 1});
// Too many separate queries
for (let id of ids) {
const user = db.find({id});
}
这无法利用索引、检索未使用的字段并执行过多的查询。
正确示例:
// Use index on 'name'
db.find({name: 'John'}).hint({name: 1});
// Only get 'email' field
db.find({first: 'John'}, {email: 1});
// Get users in one query
const users = db.find({
id: {$in: ids}
});
分析并解释计划。战略性地创建索引。避免多次零散的查询。优化数据存储交互。
14. Promise 中错误处理不当
Promise 简化了异步代码。但未经处理的拒绝就是无声的失败!
反面例子:
function getUser() {
return fetch('/user')
.then(r => r.json());
}
getUser();
如果fetch拒绝,异常就不会被注意到。
正确示例:
function getUser() {
return fetch('/user')
.then(r => r.json())
.catch(err => console.error(err));
}
getUser();
链接.catch()可以正确处理错误。
15. 同步网络操作
网络请求应该是异步的。但有时会使用同步变体:
反面例子:
const data = http.getSync('http://example.com/data'); // blocks!
这会在请求期间停止事件循环。相反,使用回调:
正确示例:
http.get('http://example.com/data', res => {
// ...
});
或者:
fetch('http://example.com/data')
.then(res => res.json())
.then(data => {
// ...
});
异步网络请求允许在等待响应时进行其他处理。避免同步网络调用。
16. 低效的文件 I/O 操作
读/写文件同步阻塞。例如:
反面例子:
const contents = fs.readFileSync('file.txt'); // blocks!
这会在磁盘 I/O 期间停止执行。
正确示例:
fs.readFile('file.txt', (err, contents) => {
// ...
});
// or promises
fs.promises.readFile('file.txt')
.then(contents => {
// ...
});
这允许事件循环在文件读取期间继续。
对于多个文件,使用流:
function processFiles(files) {
for (let file of files) {
fs.createReadStream(file)
.pipe(/*...*/);
}
}
避免同步文件操作。使用回调、promise 和流。
17. 忽略性能分析和优化
在出现明显问题之前,很容易忽视性能。但优化应该持续进行!首先使用分析工具进行测量:
- 浏览器开发工具时间线
- Node.js 分析器
- 第三方分析器
即使性能看起来不错,这也揭示了优化机会:
// profile.js
function processOrders(orders) {
orders.forEach(o => {
// ...
});
}
processOrders(allOrders);
分析器显示processOrders需要 200 毫秒。
分析指导优化。制定绩效预算,如果超出则失败。经常测量并明智地优化。
18. 不利用缓存机制
缓存通过避免重复工作来提高速度。但它经常被遗忘。
反面例子:
// Compute expensive report
function generateReport() {
// ...heavy processing...
}
generateReport(); // Computes
generateReport(); // Computes again!
相同的输入总是产生相同的输出。我们应该缓存:
正确示例:
// Cache report contents
const cache = {};
function generateReport() {
if (cache.report) {
return cache.report;
}
const report = // ...compute...
cache.report = report;
return report;
}
现在,重复调用速度很快。
19. 不必要的代码重复
重复的代码会损害可维护性和可优化性。
function userStats(user) {
const name = user.name;
const email = user.email;
// ...logic...
}
function orderStats(order) {
const name = order.customerName;
const email = order.customerEmail;
// ...logic...
}
提取是重复的。我们重来:
function getCustomerInfo(data) {
return {
name: data.name,
email: data.email
};
}
function userStats(user) {
const { name, email } = getCustomerInfo(user);
// ...logic...
}
function orderStats(order) {
const { name, email } = getCustomerInfo(order);
// ...logic...
}
现在,它只定义一次。
结论
优化 JavaScript 应用程序性能是一个迭代过程。通过学习有效的实践并勤于分析,可以显着提高速度。
需要关注的关键领域包括最大限度地减少 DOM 更改、利用异步技术、消除阻塞操作、减少依赖性、利用缓存以及删除不需要的重复。
评论 (0)