ประสิทธิภาพและ 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()
หัวข้อที่มีชื่อว่า “performance.now()”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');performance.mark() และ performance.measure()
หัวข้อที่มีชื่อว่า “performance.mark() และ performance.measure()”สำหรับการวัดที่มีชื่อและแสดงใน 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 — การเก็บข้อมูลแบบ async
หัวข้อที่มีชื่อว่า “PerformanceObserver — การเก็บข้อมูลแบบ async”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 leakobs.disconnect();Demo แบบ Live — จับเวลา CPU-bound loop
หัวข้อที่มีชื่อว่า “Demo แบบ Live — จับเวลา CPU-bound loop”Needs the Node.js runtime — open in StackBlitz to run.
การรักษา event loop ไม่ให้ถูกบล็อก
หัวข้อที่มีชื่อว่า “การรักษา event loop ไม่ให้ถูกบล็อก”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 เป็นระยะด้วย setImmediateasync 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 Threadimport { Worker } from 'node:worker_threads';// (ดูเพิ่มเติมในโมดูล Streams & Worker Threads)CPU profiling ด้วย --prof
หัวข้อที่มีชื่อว่า “CPU profiling ด้วย --prof”สำหรับการ profile CPU เชิงลึก Node มี V8 profiler ในตัว:
# รันพร้อม profilingnode --prof src/app.js
# แปลง isolate log เป็นรายงานที่อ่านได้node --prof-process isolate-*.log > profile.txtoutput จะแสดงว่า function ใดใช้ CPU time มากที่สุด (ticks) ดูที่ส่วน “Bottom up (heavy) profile” เพื่อหา hot paths