緩慢或卡頓的網(wǎng)站是業(yè)余愛(ài)好者的標(biāo)志,而流暢、優(yōu)化的體驗(yàn)會(huì)讓用戶感到高興,并使專業(yè)人士脫穎而出。
但創(chuàng)建真正高性能的網(wǎng)絡(luò)應(yīng)用程序充滿了陷阱。錯(cuò)誤比比皆是,它們可能會(huì)拖慢JavaScript的速度,而您甚至沒(méi)有意識(shí)到這一點(diǎn)。微小的疏忽會(huì)讓你的代碼變得臃腫,并悄悄地一點(diǎn)一點(diǎn)地降低速度。
這是怎么回事?
事實(shí)證明,我們有很多常見(jiàn)的方式會(huì)無(wú)意中減慢 JavaScript 的速度。隨著時(shí)間的推移,可能會(huì)阻礙網(wǎng)站性能。
這些錯(cuò)誤是可以避免的。
今天,我們重點(diǎn)關(guān)注可能會(huì)悄悄減慢 JavaScript 和 Node.js 應(yīng)用程序速度的 19 個(gè)性能陷阱。我們將通過(guò)說(shuō)明性示例和可操作的解決方案來(lái)探索導(dǎo)致這些問(wèn)題的原因,以優(yōu)化您的代碼。
識(shí)別并消除這些危害是打造讓用戶滿意的流暢網(wǎng)絡(luò)體驗(yàn)的關(guān)鍵。那么,讓我們深入了解一下吧!
1. 不正確的變量聲明和作用域
第一次學(xué)習(xí) JavaScript 時(shí),很容易在全局聲明所有變量。然而,這會(huì)導(dǎo)致未來(lái)出現(xiàn)問(wèn)題。讓我們看一個(gè)例子:
// globals.js
var color = 'blue';
function printColor() {
console.log(color);
}
printColor(); // Prints 'blue'
登錄后復(fù)制
這工作正常,但想象一下如果我們加載另一個(gè)腳本:
// script2.js var color = 'red'; printColor(); // Prints 'red'!
登錄后復(fù)制
因?yàn)閏olor是全局的,所以script2.js覆蓋了它!要解決此問(wèn)題,請(qǐng)盡可能在函數(shù)內(nèi)部聲明變量:
function printColor() {
var color = 'blue'; // local variable
console.log(color);
}
printColor(); // Prints 'blue'
登錄后復(fù)制
現(xiàn)在,其他腳本的更改不會(huì)影響printColor.
不必要時(shí)在全局范圍內(nèi)聲明變量是一種反模式。嘗試將全局變量限制為配置常量。對(duì)于其他變量,請(qǐng)?jiān)诒M可能小的范圍內(nèi)進(jìn)行本地聲明。
2. 低效的 DOM 操作
更新 DOM 元素時(shí),批量更改而不是一次操作一個(gè)節(jié)點(diǎn)。考慮這個(gè)例子:
const ul = document.getElementById('list');
for (let i = 0; i < 10; i++) {
const li = document.createElement('li');
li.textContent = i;
ul.appendChild(li);
}
登錄后復(fù)制
這將逐一附加列表項(xiàng)。最好先構(gòu)建一個(gè)字符串然后設(shè)置.innerHTML:
const ul = document.getElementById('list');
let html = '';
for (let i = 0; i < 10; i++) {
html += `<li>${i}</li>`;
}
ul.innerHTML = html;
登錄后復(fù)制
構(gòu)建字符串可以最大限度地減少回流。我們更新 DOM 一次而不是 10 次。
對(duì)于多個(gè)更新,構(gòu)建更改,然后在最后應(yīng)用?;蛘吒玫氖?,使用 DocumentFragment 批量追加。
3. 過(guò)多的 DOM 操作
頻繁的DOM更新會(huì)降低性能??紤]一個(gè)將消息插入頁(yè)面的聊天應(yīng)用程序。
反面例子:
// New message received
const msg = `<div>${messageText}</div>`;
chatLog.insertAdjacentHTML('beforeend', msg);
登錄后復(fù)制
這天真地插入到每條消息上。最好是限制更新:
正確示例:
let chatLogHTML = '';
const throttleTime = 100; // ms
// New message received
chatLogHTML += `<div>${messageText}</div>`;
// Throttle DOM updates
setTimeout(() => {
chatLog.innerHTML = chatLogHTML;
chatLogHTML = '';
}, throttleTime);
登錄后復(fù)制
現(xiàn)在,我們最多每 100 毫秒更新一次,從而保持 DOM 操作較低。
對(duì)于高度動(dòng)態(tài)的 UI,請(qǐng)考慮像 React 這樣的虛擬 DOM 庫(kù)。這些最大限度地減少了使用虛擬表示的 DOM 操作。
4.缺乏活動(dòng)委托
將事件偵聽(tīng)器附加到許多元素會(huì)產(chǎn)生不必要的開(kāi)銷??紤]一個(gè)每行都有刪除按鈕的表:
反面例子:
const rows = document.querySelectorAll('table tr');
rows.forEach(row => {
const deleteBtn = row.querySelector('.delete');
deleteBtn.addEventListener('click', handleDelete);
});
登錄后復(fù)制
這會(huì)為每個(gè)刪除按鈕添加一個(gè)偵聽(tīng)器。需要更好地使用事件委托:
正確示例:
const table = document.querySelector('table');
table.addEventListener('click', e => {
if (e.target.classList.contains('delete')) {
handleDelete(e);
}
});
登錄后復(fù)制
現(xiàn)在,.net 上只有一個(gè)偵聽(tīng)器,更少的內(nèi)存開(kāi)銷。
事件委托利用事件冒泡。一個(gè)偵聽(tīng)器可以處理來(lái)自多個(gè)后代的事件。只要適用,就使用委派。
5. 低效的字符串連接
在循環(huán)中連接字符串時(shí),性能會(huì)受到影響??紤]這段代碼:
let html = '';
for (let i = 0; i < 10; i++) {
html += '<div>' + i + '</div>';
}
登錄后復(fù)制
創(chuàng)建新字符串需要分配內(nèi)存。最好使用數(shù)組:
const parts = [];
for (let i = 0; i < 10; i++) {
parts.push('<div>', i, '</div>');
}
const html = parts.join('');
登錄后復(fù)制
構(gòu)建數(shù)組可以最大限度地減少中間字符串。.join()最后連接一次。
對(duì)于多個(gè)字符串添加,請(qǐng)使用數(shù)組連接。另外,請(qǐng)考慮嵌入值的模板文字。
6. 未優(yōu)化的循環(huán)
JavaScript 中的循環(huán)經(jīng)常會(huì)導(dǎo)致性能問(wèn)題。一個(gè)常見(jiàn)的錯(cuò)誤是重復(fù)訪問(wèn)數(shù)組長(zhǎng)度:
反面例子:
const items = [/*...*/];
for (let i = 0; i < items.length; i++) {
// ...
}
登錄后復(fù)制
冗余檢查.length會(huì)抑制優(yōu)化。
正確示例:
const items = [/*...*/];
const len = items.length;
for (let i = 0; i < len; i++) {
// ...
}
登錄后復(fù)制
緩存長(zhǎng)度可以提高速度。其他優(yōu)化包括將不變量提升到循環(huán)之外、簡(jiǎn)化終止條件以及避免迭代內(nèi)昂貴的操作。
7. 不必要的同步操作
JavaScript 的異步功能是一個(gè)關(guān)鍵優(yōu)勢(shì)。但要小心阻塞 I/O!例如:
反面例子:
const data = fs.readFileSync('file.json'); // blocks!
登錄后復(fù)制
這會(huì)在從磁盤(pán)讀取時(shí)停止執(zhí)行。相反,如果使用回調(diào)或承諾:
正確示例:
fs.readFile('file.json', (err, data) => {
// ...
});
登錄后復(fù)制
現(xiàn)在,事件循環(huán)在讀取文件時(shí)繼續(xù)。對(duì)于復(fù)雜的流程,async/await簡(jiǎn)化異步邏輯。避免同步操作以防止阻塞。
8. 阻止事件循環(huán)
JavaScript 使用單線程事件循環(huán)。阻止它會(huì)停止執(zhí)行。一些常見(jiàn)的攔截器:
繁重的計(jì)算任務(wù)
同步輸入/輸出
未優(yōu)化的算法
例如:
function countPrimes(max) {
// Unoptimized loop
for (let i = 0; i <= max; i++) {
// ...check if prime...
}
}
countPrimes(1000000); // Long running!
登錄后復(fù)制
這會(huì)同步執(zhí)行,并阻止其他事件。避免:
推遲不必要的工作
批量數(shù)據(jù)處理
使用工作線程
尋找優(yōu)化機(jī)會(huì)
保持事件循環(huán)順利運(yùn)行。定期分析以捕獲阻塞代碼。
9. 錯(cuò)誤處理效率低下
在 JavaScript 中正確處理錯(cuò)誤至關(guān)重要。但要小心性能陷阱!
反面例子:
try {
// ...
} catch (err) {
console.error(err); // just logging
}
登錄后復(fù)制
這會(huì)捕獲錯(cuò)誤但不采取糾正措施。未處理的錯(cuò)誤通常會(huì)導(dǎo)致內(nèi)存泄漏或數(shù)據(jù)損壞。
正確示例:
try {
// ...
} catch (err) {
console.error(err);
// Emit error event
emitError(err);
// Nullify variables
obj = null;
// Inform user
showErrorNotice();
}
登錄后復(fù)制
記錄還不夠!清理工件、通知用戶并考慮恢復(fù)選項(xiàng)。使用 Sentry 等工具來(lái)監(jiān)控生產(chǎn)中的錯(cuò)誤。明確處理所有錯(cuò)誤。
10. 內(nèi)存泄漏
當(dāng)內(nèi)存被分配但從未釋放時(shí),就會(huì)發(fā)生內(nèi)存泄漏。隨著時(shí)間的推移,泄漏會(huì)累積并降低性能。
JavaScript 中的常見(jiàn)來(lái)源包括:
未清理的事件監(jiān)聽(tīng)器
對(duì)已刪除 DOM 節(jié)點(diǎn)的過(guò)時(shí)引用
不再需要的緩存數(shù)據(jù)
閉包中的累積狀態(tài)
例如:
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!
登錄后復(fù)制
數(shù)組不斷變大,但從未被清除。修理:
使用弱引用
清理事件監(jiān)聽(tīng)器
刪除不再需要的引用
限制關(guān)閉狀態(tài)大小
監(jiān)視內(nèi)存使用情況并觀察增長(zhǎng)趨勢(shì)。在泄漏堆積之前主動(dòng)消除泄漏。
11. 過(guò)度使用依賴項(xiàng)
雖然 npm 提供了無(wú)窮無(wú)盡的選擇,但請(qǐng)抵制過(guò)度導(dǎo)入的沖動(dòng)!每個(gè)依賴項(xiàng)都會(huì)增加包大小和攻擊面。
反面例子:
import _ from 'lodash'; import moment from 'moment'; import validator from 'validator'; // etc...
登錄后復(fù)制
為次要實(shí)用程序?qū)胝麄€(gè)庫(kù)。最好根據(jù)需要挑選助手:
正確示例:
import cloneDeep from 'lodash/cloneDeep';
import { format } from 'date-fns';
import { isEmail } from 'validator';
登錄后復(fù)制
只導(dǎo)入您需要的內(nèi)容。定期檢查依賴關(guān)系以刪除未使用的依賴關(guān)系。保持捆綁精簡(jiǎn)并最大限度地減少依賴性。
12. 緩存不足
緩存允許通過(guò)重用先前的結(jié)果來(lái)跳過(guò)昂貴的計(jì)算。但它經(jīng)常被忽視。
反面例子:
function generateReport() {
// Perform expensive processing
// to generate report data...
}
generateReport(); // Computes
generateReport(); // Computes again!
登錄后復(fù)制
由于輸入沒(méi)有更改,因此可以緩存報(bào)告:
正確示例:
let cachedReport;
function generateReport() {
if (cachedReport) {
return cachedReport;
}
cachedReport = // expensive processing...
return cachedReport;
}
登錄后復(fù)制
現(xiàn)在,重復(fù)調(diào)用速度很快。
13. 未優(yōu)化的數(shù)據(jù)庫(kù)查詢
與數(shù)據(jù)庫(kù)交互時(shí),低效的查詢可能會(huì)降低性能。需要避免的一些問(wèn)題:
反面例子:
// No indexing
db.find({name: 'John', age: 35});
// Unecessary fields
db.find({first: 'John', last:'Doe', email:'[email protected]'}, {first: 1, last: 1});
// Too many separate queries
for (let id of ids) {
const user = db.find({id});
}
登錄后復(fù)制
這無(wú)法利用索引、檢索未使用的字段并執(zhí)行過(guò)多的查詢。
正確示例:
// 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}
});
登錄后復(fù)制
分析并解釋計(jì)劃。戰(zhàn)略性地創(chuàng)建索引。避免多次零散的查詢。優(yōu)化數(shù)據(jù)存儲(chǔ)交互。
14. Promise 中錯(cuò)誤處理不當(dāng)
Promise 簡(jiǎn)化了異步代碼。但未經(jīng)處理的拒絕就是無(wú)聲的失??!
反面例子:
function getUser() {
return fetch('/user')
.then(r => r.json());
}
getUser();
登錄后復(fù)制
如果fetch拒絕,異常就不會(huì)被注意到。
正確示例:
function getUser() {
return fetch('/user')
.then(r => r.json())
.catch(err => console.error(err));
}
getUser();
登錄后復(fù)制
鏈接.catch()可以正確處理錯(cuò)誤。
15. 同步網(wǎng)絡(luò)操作
網(wǎng)絡(luò)請(qǐng)求應(yīng)該是異步的。但有時(shí)會(huì)使用同步變體:
反面例子:
const data = http.getSync('http://example.com/data'); // blocks!
登錄后復(fù)制
這會(huì)在請(qǐng)求期間停止事件循環(huán)。相反,使用回調(diào):
正確示例:
http.get('http://example.com/data', res => {
// ...
});
登錄后復(fù)制
或者:
fetch('http://example.com/data')
.then(res => res.json())
.then(data => {
// ...
});
登錄后復(fù)制
異步網(wǎng)絡(luò)請(qǐng)求允許在等待響應(yīng)時(shí)進(jìn)行其他處理。避免同步網(wǎng)絡(luò)調(diào)用。
16. 低效的文件 I/O 操作
讀/寫(xiě)文件同步阻塞。例如:
反面例子:
const contents = fs.readFileSync('file.txt'); // blocks!
登錄后復(fù)制
這會(huì)在磁盤(pán) I/O 期間停止執(zhí)行。
正確示例:
fs.readFile('file.txt', (err, contents) => {
// ...
});
// or promises
fs.promises.readFile('file.txt')
.then(contents => {
// ...
});
登錄后復(fù)制
這允許事件循環(huán)在文件讀取期間繼續(xù)。
對(duì)于多個(gè)文件,使用流:
function processFiles(files) {
for (let file of files) {
fs.createReadStream(file)
.pipe(/*...*/);
}
}
登錄后復(fù)制
避免同步文件操作。使用回調(diào)、promise 和流。
17. 忽略性能分析和優(yōu)化
在出現(xiàn)明顯問(wèn)題之前,很容易忽視性能。但優(yōu)化應(yīng)該持續(xù)進(jìn)行!首先使用分析工具進(jìn)行測(cè)量:
瀏覽器開(kāi)發(fā)工具時(shí)間線
Node.js 分析器
第三方分析器
即使性能看起來(lái)不錯(cuò),這也揭示了優(yōu)化機(jī)會(huì):
// profile.js
function processOrders(orders) {
orders.forEach(o => {
// ...
});
}
processOrders(allOrders);
登錄后復(fù)制
分析器顯示processOrders需要 200 毫秒。
分析指導(dǎo)優(yōu)化。制定績(jī)效預(yù)算,如果超出則失敗。經(jīng)常測(cè)量并明智地優(yōu)化。
18. 不利用緩存機(jī)制
緩存通過(guò)避免重復(fù)工作來(lái)提高速度。但它經(jīng)常被遺忘。
反面例子:
// Compute expensive report
function generateReport() {
// ...heavy processing...
}
generateReport(); // Computes
generateReport(); // Computes again!
登錄后復(fù)制
相同的輸入總是產(chǎn)生相同的輸出。我們應(yīng)該緩存:
正確示例:
// Cache report contents
const cache = {};
function generateReport() {
if (cache.report) {
return cache.report;
}
const report = // ...compute...
cache.report = report;
return report;
}
登錄后復(fù)制
現(xiàn)在,重復(fù)調(diào)用速度很快。
19. 不必要的代碼重復(fù)
重復(fù)的代碼會(huì)損害可維護(hù)性和可優(yōu)化性。
function userStats(user) {
const name = user.name;
const email = user.email;
// ...logic...
}
function orderStats(order) {
const name = order.customerName;
const email = order.customerEmail;
// ...logic...
}
登錄后復(fù)制
提取是重復(fù)的。我們重來(lái):
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...
}
登錄后復(fù)制
現(xiàn)在,它只定義一次。
結(jié)論
優(yōu)化 JavaScript 應(yīng)用程序性能是一個(gè)迭代過(guò)程。通過(guò)學(xué)習(xí)有效的實(shí)踐并勤于分析,可以顯著提高速度。
需要關(guān)注的關(guān)鍵領(lǐng)域包括最大限度地減少 DOM 更改、利用異步技術(shù)、消除阻塞操作、減少依賴性、利用緩存以及刪除不需要的重復(fù)。
以上就是代碼運(yùn)行慢?避免這19個(gè)常見(jiàn)的JavaScript和Node.js錯(cuò)誤,讓你的程序高速狂飆的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注www.92cms.cn其它相關(guān)文章!






