ข้ามไปยังเนื้อหา

ประสิทธิภาพและ perf_hooks

“การ optimize ก่อนเวลาอันควรคือต้นตอของความชั่วทั้งปวง” — Donald Knuth

ก่อนที่คุณจะ optimize อะไรก็ตาม ต้องวัดก่อนเสมอ โมดูล perf_hooks ของ Node ให้การจับเวลาแบบ sub-millisecond ผ่าน High Resolution Time API — performance.now() เดียวกับที่คุณรู้จักจาก browser แต่ใช้ได้ใน Node แบบ native และ PerformanceObserver สำหรับการรับ entry แบบ async

performance.now() คืนค่า float ในหน่วย milliseconds ที่มีความแม่นยำระดับ microsecond เป็น monotonic — ไม่เคยถอยหลัง แม้ระบบนาฬิกาจะถูกปรับ:

import { performance } from 'node:perf_hooks';
const start = performance.now();
// --- งานที่ต้องการวัด ---
let sum = 0;
for (let i = 0; i < 1_000_000; i++) {
sum += i;
}
// ----------------------
const elapsed = performance.now() - start;
console.log('Sum loop took: ' + elapsed.toFixed(3) + ' ms');

สำหรับการวัดที่มีชื่อและแสดงใน DevTools timelines:

import { performance, PerformanceObserver } from 'node:perf_hooks';
performance.mark('db-start');
// ... จำลอง DB query ...
await new Promise(function(resolve) { setTimeout(resolve, 50); });
performance.mark('db-end');
performance.measure('database-query', 'db-start', 'db-end');
const entries = performance.getEntriesByName('database-query');
console.log(entries[0].duration.toFixed(2) + ' ms');

PerformanceObserver เรียก callback เมื่อมี performance entry ใหม่ถูกสร้างขึ้น มีประโยชน์สำหรับการวัด library code ที่คุณไม่ได้ควบคุม:

import { PerformanceObserver, performance } from 'node:perf_hooks';
const obs = new PerformanceObserver(function(list) {
for (const entry of list.getEntries()) {
console.log(entry.name + ': ' + entry.duration.toFixed(2) + ' ms');
}
});
obs.observe({ entryTypes: ['measure'] });
performance.mark('a');
// ... ทำงาน ...
performance.mark('b');
performance.measure('my-operation', 'a', 'b');
// disconnect เสมอเมื่อเสร็จแล้ว เพื่อป้องกัน memory leak
obs.disconnect();
Node.js

Needs the Node.js runtime — open in StackBlitz to run.

CPU-bound loop ที่ทำงานนานกว่า ~50 ms จะบล็อก event loop — ไม่มี I/O, ไม่มี timer, ไม่มี request อื่นที่ประมวลผลได้ในระหว่างนั้น

// แย่ — บล็อก event loop ตลอดเวลา
function blockingWork(data) {
const result = [];
for (const item of data) {
result.push(expensiveTransform(item)); // แต่ละ call ใช้เวลา ~1ms, 10000 items = บล็อก 10s
}
return result;
}
// ดีกว่า — yield control เป็นระยะด้วย setImmediate
async function nonBlockingWork(data) {
const result = [];
for (let i = 0; i < data.length; i++) {
result.push(expensiveTransform(data[i]));
if (i % 100 === 0) {
// Yield ให้ event loop ทุก 100 items
await new Promise(function(resolve) { setImmediate(resolve); });
}
}
return result;
}
// ดีที่สุดสำหรับงาน CPU-heavy — offload ไปยัง Worker Thread
import { Worker } from 'node:worker_threads';
// (ดูเพิ่มเติมในโมดูล Streams & Worker Threads)

สำหรับการ profile CPU เชิงลึก Node มี V8 profiler ในตัว:

Terminal window
# รันพร้อม profiling
node --prof src/app.js
# แปลง isolate log เป็นรายงานที่อ่านได้
node --prof-process isolate-*.log > profile.txt

output จะแสดงว่า function ใดใช้ CPU time มากที่สุด (ticks) ดูที่ส่วน “Bottom up (heavy) profile” เพื่อหา hot paths

`performance.now()` คืนค่าในหน่วยอะไร?
ทำไม `performance.now()` ถึงเหมาะสำหรับการ benchmark มากกว่า `Date.now()`?
CPU-bound loop ที่ทำงานนานทำให้เกิดปัญหาอะไรใน Node.js?
เทคนิคใดที่ yield control กลับให้ event loop เป็นระยะระหว่าง loop ขนาดใหญ่?