ถ้าคุณกำลังนั่งเครียดกับค่า INP (Interaction to Next Paint) ที่พุ่งสูงในปี 2026 และพบว่า PerformanceObserver แบบเดิมที่ดู Long Tasks ให้ข้อมูลแบบ "รู้ว่าช้า แต่ไม่รู้ว่าใครทำ" บอกเลยว่ามาถูกที่แล้ว Long Animation Frames API หรือที่หลายคนเรียกสั้นๆ ว่า LoAF คือเครื่องมือที่ผมอยากให้คุณรู้จัก
บทความนี้จะพาคุณวินิจฉัยปัญหา Main Thread Blocking แบบเจาะลึก พร้อมโค้ดจริงที่ผมเองใช้ในงาน production มาแล้วหลายโปรเจกต์ — เอาไปใช้ได้เลย ไม่ต้องดัดแปลงเยอะ
Long Animation Frames API คืออะไร และทำไมถึงดีกว่า Long Tasks API
LoAF เป็น Web API ที่เปิดตัวครั้งแรกใน Chrome 123 (ใครจำได้บ้าง?) และมาถึงปี 2026 ตอนนี้รองรับเป็นมาตรฐานทั้งใน Chrome, Edge และ Opera ครอบคลุมผู้ใช้ทั่วโลกมากกว่า 75% ของ Browser Market Share — ตัวเลขที่บอกได้เลยว่า "ใช้ได้แบบสบายใจ"
ความแตกต่างหลักระหว่าง Long Animation Frames กับ Long Tasks API แบบเดิมที่หลายคนคุ้นเคย:
- Long Tasks API รายงานเฉพาะ task ที่ใช้เวลามากกว่า 50ms แต่ไม่บอกอะไรเลยว่าเป็น script ตัวไหน หรือ rendering phase ไหนกันแน่ (เหมือนหมอบอกว่า "คุณป่วย" แต่ไม่บอกโรค)
- Long Animation Frames API รายงาน frame ทั้งหมดที่ใช้เวลามากกว่า 50ms พร้อมรายละเอียดครบ ทั้ง script attribution, render time, style/layout duration และ blocking duration
พูดตามตรง ในยุคที่ INP กลายเป็น Core Web Vitals หลักแทน FID ตั้งแต่ปี 2024 การจะรู้ว่า frame ไหนทำให้ interaction ช้านั้นสำคัญมาก LoAF ตอบโจทย์ตรงจุด เพราะมันรู้ลึกถึงระดับว่า frame ไหนรวม render หรือ layout work ที่หนัก ไม่ใช่แค่ JavaScript task อย่างเดียว
โครงสร้างข้อมูลของ PerformanceLongAnimationFrameTiming
ทีนี้มาดูของจริงกันบ้าง เมื่อคุณ subscribe LoAF entries ผ่าน PerformanceObserver คุณจะได้ object หน้าตาประมาณนี้:
{
"name": "long-animation-frame",
"entryType": "long-animation-frame",
"startTime": 1234.5,
"duration": 187,
"renderStart": 1356.2,
"styleAndLayoutStart": 1390.8,
"firstUIEventTimestamp": 1240.1,
"blockingDuration": 142,
"scripts": [
{
"name": "script",
"entryType": "script",
"startTime": 1235.0,
"duration": 95,
"invoker": "https://example.com/app.js",
"invokerType": "user-callback",
"executionStart": 1235.5,
"forcedStyleAndLayoutDuration": 12,
"pauseDuration": 0,
"sourceURL": "https://example.com/app.js",
"sourceFunctionName": "handleClick",
"sourceCharPosition": 4521
}
]
}
ความหมายของแต่ละ field (แบบสรุปสั้นๆ)
- duration: ระยะเวลารวมของ frame นี้ ตั้งแต่เริ่มจนจบ paint
- blockingDuration: ระยะเวลาที่ Main Thread ถูก block จากงานอื่น (เกิน 50ms) — ตัวนี้แหละที่ผมดูเป็นอย่างแรกเสมอ
- renderStart: เวลาที่ browser เริ่ม rendering work (style, layout, paint)
- styleAndLayoutStart: เวลาที่ style และ layout เริ่มประมวลผล
- scripts[]: array ของ script ที่ทำงานใน frame นี้ พร้อม source URL และ function name (ฮีโร่ของเรานั่นเอง)
วิธีใช้ Long Animation Frames API ใน Production
1. ตั้งค่า PerformanceObserver พื้นฐาน
เอาล่ะ มาเขียนโค้ดกันเลย โค้ดต่อไปนี้จะ subscribe LoAF entries แล้วส่งไปยัง analytics endpoint ของคุณ:
if ('PerformanceObserver' in window &&
PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
reportLoAF({
duration: entry.duration,
blockingDuration: entry.blockingDuration,
renderStart: entry.renderStart,
scripts: entry.scripts.map(s => ({
url: s.sourceURL,
functionName: s.sourceFunctionName,
duration: s.duration,
invokerType: s.invokerType,
})),
});
}
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
}
function reportLoAF(data) {
navigator.sendBeacon('/api/perf/loaf', JSON.stringify(data));
}
สังเกตว่าเราใช้ buffered: true เพื่อรับ entries ที่เกิดขึ้นก่อนที่ observer จะถูกสร้าง (สำคัญมาก! ผมเคยลืมแล้วงงอยู่นานว่าทำไมข้อมูลหาย) และใช้ navigator.sendBeacon แทน fetch เพราะมันส่งข้อมูลได้สำเร็จแม้ผู้ใช้กำลังกดปิดหน้า
2. ผูก LoAF เข้ากับ INP measurements
ทีนี้เพื่อให้ได้ข้อมูลที่ actionable จริงๆ คุณควร correlate LoAF entry กับ INP event ที่เกิดในช่วงเวลาเดียวกัน — นี่คือจุดที่ทำให้ผมโยน Long Tasks ทิ้งไปเลย:
import { onINP } from 'web-vitals/attribution';
const loafEntries = [];
new PerformanceObserver((list) => {
loafEntries.push(...list.getEntries());
if (loafEntries.length > 100) loafEntries.shift();
}).observe({ type: 'long-animation-frame', buffered: true });
onINP((metric) => {
const inpStart = metric.entries[0].startTime;
const inpEnd = inpStart + metric.value;
const relatedFrames = loafEntries.filter(loaf =>
loaf.startTime < inpEnd &&
(loaf.startTime + loaf.duration) > inpStart
);
reportINPWithLoAF({
inp: metric.value,
rating: metric.rating,
target: metric.attribution.interactionTarget,
frames: relatedFrames.map(f => ({
duration: f.duration,
blockingDuration: f.blockingDuration,
culprit: f.scripts[0]?.sourceURL,
})),
});
});
วิธีนี้จะบอกคุณตรงๆ ว่า INP event ที่ช้านั้นเกิดจาก script ไฟล์ไหน function ชื่ออะไร เปลี่ยนการ debug จากการ "เดา" เป็นการ "ฟันธง" เลยทีเดียว
5 รูปแบบ Long Animation Frame ที่พบบ่อย และวิธีแก้
รูปแบบที่ 1: Heavy event handler
เมื่อ invokerType เป็น event-listener และ script duration สูง ปัญหาอยู่ที่ event handler โดยตรง วิธีแก้คือใช้ scheduler.yield() หรือ requestIdleCallback เพื่อแบ่งงาน:
async function handleSearchInput(query) {
const tokens = tokenize(query);
await scheduler.yield();
const filtered = filterResults(tokens);
await scheduler.yield();
renderResults(filtered);
}
รูปแบบที่ 2: Forced reflow / Layout thrashing
หาก forcedStyleAndLayoutDuration สูง แสดงว่า script อ่านค่า DOM property (เช่น offsetWidth, getBoundingClientRect) สลับกับการเขียน DOM จนทำให้ browser ต้อง recalculate layout ซ้ำๆ ปัญหานี้ผมเจอบ่อยมากในโค้ด legacy
วิธีแก้ก็ตรงไปตรงมา: แยกการอ่านและการเขียน DOM ออกจากกัน ใช้ requestAnimationFrame ช่วย
const widths = [];
elements.forEach(el => widths.push(el.offsetWidth));
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.width = (widths[i] * 1.1) + 'px';
});
});
รูปแบบที่ 3: Third-party script ทำงานหนัก
หาก sourceURL ชี้ไปที่ domain ของ third party (analytics, ads, chat widget — ตัวร้ายตัวจริง) ให้พิจารณาใช้ type="module" + async หรือโหลดผ่าน Web Worker / Partytown เพื่อย้าย script ออกจาก main thread ไปเลย
รูปแบบที่ 4: Render-blocking ใน frame
หาก renderStart - startTime ใหญ่กว่า styleAndLayoutStart - renderStart มาก แปลว่า script ใน frame นั้นเยอะเกินไปจน browser ไม่ได้ render เลยจนกว่า script จะจบ
วิธีแก้: ใช้ Element.animate() แทน CSS class toggling สำหรับ animation และย้าย heavy compute ไป Web Worker
รูปแบบที่ 5: Long task หลังจาก rAF callback
หาก invokerType เป็น requestAnimationFrame และ duration สูง animation ของคุณกำลัง schedule งานหนักทุกเฟรม ลองเปลี่ยนเป็น CSS animation หรือ Element.animate() ที่รันบน Compositor Thread แทน — ผลต่าง? เห็นได้ทันทีบนมือถือกลางๆ
ข้อควรระวังและ Best Practices
Sample rate ในการเก็บข้อมูล
การเก็บ LoAF entries ทุกครั้งจะส่งผลกระทบต่อ network เล็กน้อย ในไซต์ traffic สูงควร sample แค่ 10-25% ของ session ก็พอ:
const SAMPLE_RATE = 0.1;
const shouldSample = Math.random() < SAMPLE_RATE;
if (shouldSample) {
observer.observe({ type: 'long-animation-frame', buffered: true });
}
Filter LoAF entries ที่เกิดในช่วง idle
Frame ที่ duration สูงแต่ firstUIEventTimestamp ไม่มีค่า แสดงว่าผู้ใช้ไม่ได้ interact จริงๆ ดังนั้นไม่กระทบ INP โดยตรง ให้ filter ออกเพื่อลด noise ในข้อมูลของเรา:
if (entry.firstUIEventTimestamp === 0) return;
Browser Compatibility
ปี 2026 LoAF รองรับใน Chrome 123+, Edge 123+, Opera 109+ ส่วน Safari และ Firefox ยังไม่ implement (อย่ารอเลย เริ่มเก็บก่อนได้) ซึ่งสอดคล้องกับ Chrome User Experience Report (CrUX) ที่ใช้วัด Core Web Vitals อยู่แล้ว metric ที่คุณเก็บจึง represent ผู้ใช้ส่วนใหญ่ได้สบาย
เปรียบเทียบ Long Animation Frames กับเครื่องมือ Profiling อื่น
| Feature | LoAF API | Long Tasks API | Chrome DevTools Performance |
|---|---|---|---|
| Script attribution | มี | ไม่มี | มี |
| Render time breakdown | มี | ไม่มี | มี |
| Production monitoring | ใช้ได้ | ใช้ได้ | เฉพาะ debugging |
| Forced reflow detection | มี | ไม่มี | มี |
| Overhead | ต่ำ | ต่ำมาก | สูง |
สรุปสั้นๆ: LoAF เหมาะสุดสำหรับ RUM (Real User Monitoring) ส่วน DevTools เก็บไว้ debug แบบ deep dive ทีหลัง
Integration กับ Analytics Tools ยอดนิยม
Google Analytics 4
function reportLoAFToGA4(entry) {
gtag('event', 'long_animation_frame', {
duration: Math.round(entry.duration),
blocking_duration: Math.round(entry.blockingDuration),
culprit_url: entry.scripts[0]?.sourceURL || 'unknown',
invoker_type: entry.scripts[0]?.invokerType || 'unknown',
});
}
SpeedCurve และ DataDog RUM
ทั้งสอง vendor รองรับการรับ custom event เรียบร้อย ใน DataDog ใช้ datadogRum.addAction() ส่วน SpeedCurve ใช้ผ่าน LUX.addData() ตั้งค่าครั้งเดียวจบ
คำถามที่พบบ่อย
Long Animation Frames API ใช้แทน Long Tasks API ได้เลยไหม
ส่วนใหญ่ใช่ครับ เพราะ LoAF ให้ข้อมูลครอบคลุมกว่ามาก แต่หากคุณยังต้องรองรับ Safari หรือ Firefox ในระยะสั้น Long Tasks API ยังจำเป็นเป็น fallback อยู่ เนื่องจาก LoAF ตอนนี้ยังเป็น Chromium-only
ค่า blockingDuration ต่างกับ duration อย่างไร
blockingDuration คือระยะเวลาที่งานบล็อก main thread เกิน 50ms (คล้าย Total Blocking Time) ส่วน duration คือเวลารวมของ frame ทั้งหมด ตั้งแต่ task เริ่มจนจบ paint หาก blockingDuration สูง เตรียมรับมือเลย — frame นี้ทำให้ user interaction ช้าแน่ๆ
ควรตั้ง threshold เท่าไหร่ในการเก็บ LoAF entries
โดย default browser จะ report เฉพาะ frame ที่นานกว่า 50ms อยู่แล้ว สำหรับ production monitoring ผมแนะนำให้เก็บทั้งหมดแล้ว filter ฝั่ง backend จะยืดหยุ่นกว่า แต่ถ้าอยากลด volume ให้เก็บเฉพาะ frame ที่ blockingDuration มากกว่า 100ms ก็พอ (เป็นขีดที่กระทบ INP อย่างชัดเจน)
LoAF ทำงานใน iframe หรือเปล่า
ทำงานครับ แต่แต่ละ iframe จะ report เฉพาะ frame ของตัวเอง ถ้าคุณมี cross-origin iframe ที่หนัก คุณจะไม่เห็น script จาก iframe ใน parent context — เป็นข้อจำกัดด้าน security ที่ทำอะไรไม่ได้
การเก็บ LoAF กระทบ performance ของไซต์ไหม
กระทบน้อยมาก เพราะ browser collect data อยู่แล้วเป็น overhead ของ rendering pipeline การ subscribe ผ่าน PerformanceObserver มีต้นทุนต่ำกว่า 1ms ต่อ frame ผมเคยทดสอบใน Chrome DevTools เปรียบเทียบ before/after พบว่าผลกระทบน้อยกว่า 0.5% ซึ่งแทบไม่มีนัยสำคัญ
สรุป
Long Animation Frames API คือเครื่องมือที่ขาดไม่ได้สำหรับการเพิ่มประสิทธิภาพ INP และ Core Web Vitals ในปี 2026 ด้วยข้อมูลที่ละเอียดทั้ง script attribution, render time และ forced reflow คุณสามารถระบุปัญหาได้แม่นยำกว่า Long Tasks API หลายเท่า
เริ่มต้นง่ายๆ ด้วยการ instrument ผ่าน PerformanceObserver, sample 10-25% ของ traffic และ correlate กับ INP measurements เท่านี้ทีม dev ของคุณก็จะรู้แน่ชัดว่าโค้ดบรรทัดไหนทำให้ผู้ใช้รู้สึกว่าเว็บช้า ไม่ต้องเดาอีกต่อไป