บทนำ: INP คืออะไร และทำไมจึงสำคัญในปี 2026
ลองนึกภาพสถานการณ์นี้ดู: ผู้ใช้คลิกปุ่ม "เพิ่มลงตะกร้า" บนเว็บไซต์ของคุณ แล้วก็... ไม่มีอะไรเกิดขึ้น ต้องรอนานกว่า 500 มิลลิวินาทีกว่าหน้าจอจะตอบสนอง ระหว่างที่รอ ผู้ใช้ก็คลิกซ้ำ กดปุ่มย้อนกลับ หรือแย่กว่านั้น — ออกจากเว็บไซต์ไปเลย นี่แหละคือปัญหาที่ Interaction to Next Paint (INP) ถูกสร้างมาเพื่อวัดและแก้ไข
INP เป็นตัวชี้วัด Core Web Vitals ที่วัด ความตอบสนองโดยรวม (overall responsiveness) ของหน้าเว็บตลอดทั้งวงจรชีวิตของการใช้งาน ต่างจาก First Input Delay (FID) ที่วัดแค่การโต้ตอบครั้งแรก INP เข้ามาแทนที่ FID อย่างเป็นทางการตั้งแต่เดือน มีนาคม 2024 โดย INP จะติดตามทุกการคลิก ทุกการแตะ และทุกการกดแป้นพิมพ์ตลอดเวลาที่ผู้ใช้อยู่บนหน้าเว็บ แล้วรายงานค่าการโต้ตอบที่ช้าที่สุด (หรือใกล้เคียงที่สุด) เป็นค่า INP ของหน้านั้น
เกณฑ์ของ INP แบ่งเป็นสามระดับ:
- ดี (Good): น้อยกว่า 200 มิลลิวินาที
- ต้องปรับปรุง (Needs Improvement): 200-500 มิลลิวินาที
- แย่ (Poor): มากกว่า 500 มิลลิวินาที
ในปี 2026 ความสำคัญของ INP ยิ่งทวีคูณขึ้นไปอีก ข้อมูลจาก Chrome User Experience Report (CrUX) ชี้ว่า 43% ของเว็บไซต์ยังไม่ผ่านเกณฑ์ INP — พูดง่าย ๆ ก็คือเกือบครึ่งหนึ่งของเว็บทั่วโลกยังมีปัญหาความตอบสนองที่ผู้ใช้รู้สึกได้ชัดเจน และเว็บไซต์ที่ผ่านเกณฑ์ Core Web Vitals ทั้งหมดก็มี อัตราการตีกลับ (bounce rate) ต่ำกว่า 24% เมื่อเทียบกับเว็บไซต์ที่ไม่ผ่าน ตัวเลขนี้ไม่ใช่แค่สถิติทางเทคนิค แต่ส่งผลตรง ๆ ต่อรายได้เลยนะ
อีกเรื่องที่น่าสนใจคือ Firefox เริ่มรองรับ INP ตั้งแต่เวอร์ชัน 144 (ตุลาคม 2025) ทำให้ INP กลายเป็นตัวชี้วัดที่ได้รับการสนับสนุนข้ามเบราว์เซอร์อย่างแท้จริงแล้ว เมื่อรวมกับข้อเท็จจริงที่ว่า อุปกรณ์มือถือคิดเป็น 58% ของทราฟฟิกเว็บทั้งหมดในปี 2026 จะเห็นได้ว่าการเพิ่มประสิทธิภาพ INP เป็นสิ่งที่ทุกทีมพัฒนาเว็บมองข้ามไม่ได้อีกต่อไป
งั้นมาเริ่มกันเลย — บทความนี้จะพาคุณเจาะลึกทุกแง่มุมของ INP ตั้งแต่ทำความเข้าใจกลไกภายใน ไปจนถึงเทคนิคขั้นสูง พร้อมตัวอย่างโค้ดที่เอาไปใช้งานได้จริง
ทำความเข้าใจสามเฟสของ INP
ก่อนจะลงมือแก้ไขอะไร เราต้องเข้าใจก่อนว่า INP มันวัดอะไรกันแน่ เวลาที่ INP รายงานนั้นประกอบด้วย สามเฟส (three phases) ที่เกิดขึ้นตั้งแต่ผู้ใช้โต้ตอบจนถึงเบราว์เซอร์วาดภาพบนหน้าจอ:
1. Input Delay (ความล่าช้าของอินพุต)
เฟสแรกคือช่วงเวลาตั้งแต่ผู้ใช้โต้ตอบ (คลิกเมาส์ แตะหน้าจอ กดแป้นพิมพ์) จนถึงเวลาที่ event handler เริ่มทำงาน ความล่าช้าตรงนี้เกิดขึ้นเมื่อ main thread กำลังยุ่งกับงานอื่นอยู่ — อาจเป็น JavaScript จาก third-party script หรือ task ที่ใช้เวลานาน ยิ่ง main thread ยุ่งมาก input delay ก็จะยิ่งนานขึ้นตามไปด้วย
สาเหตุหลัก ๆ ที่ทำให้ Input Delay สูง:
- Long Tasks ที่กำลังรันอยู่บน main thread
- Third-party scripts ที่บล็อก main thread
- การโหลดและ parse JavaScript ขนาดใหญ่
- Garbage collection ที่เกิดขึ้นในจังหวะไม่เหมาะสม
2. Processing Time (เวลาประมวลผล)
เฟสที่สองคือเวลาที่ event handler ใช้ในการทำงานจริง ๆ ช่วงนี้คือตอนที่โค้ด JavaScript ของคุณตอบสนองต่อการโต้ตอบ ไม่ว่าจะเป็นอัปเดต state, เปลี่ยนแปลง DOM หรือคำนวณค่าต่าง ๆ ถ้า event handler ทำงานหนักเกินไป เวลาเฟสนี้ก็จะยาวนานขึ้นตามธรรมดา
สาเหตุหลัก ๆ ของ Processing Time ที่สูง:
- Event handlers ที่มีการคำนวณหนัก
- การอัปเดต DOM จำนวนมากแบบ synchronous
- การเรียก API แบบ synchronous
- Layout thrashing (อ่านและเขียน DOM สลับกันซ้ำ ๆ)
3. Presentation Delay (ความล่าช้าในการแสดงผล)
เฟสสุดท้ายคือช่วงตั้งแต่ event handler ทำงานเสร็จจนถึงเวลาที่เบราว์เซอร์วาดเฟรมถัดไปบนหน้าจอ ในช่วงนี้เบราว์เซอร์ต้องคำนวณ style, layout, paint และ composite ยิ่ง DOM มีขนาดใหญ่ หรือมี layout changes เยอะ ก็ยิ่งช้า
สาเหตุหลักของ Presentation Delay ที่สูง:
- DOM ที่มีขนาดใหญ่มาก (มากกว่า 1,500 โหนด)
- CSS properties ที่มีต้นทุนสูง เช่น
box-shadowหรือfilter - Forced reflow จากการเปลี่ยนแปลง layout
- Render-blocking resources ที่ยังโหลดไม่เสร็จ
การเข้าใจว่าปัญหา INP ของคุณเกิดจากเฟสไหนสำคัญมาก เพราะวิธีแก้ไขของแต่ละเฟสนั้นแตกต่างกันโดยสิ้นเชิง
สาเหตุทั่วไปที่ทำให้คะแนน INP แย่
จากการวิเคราะห์เว็บไซต์หลายพันแห่ง นี่คือสาเหตุที่พบบ่อยที่สุด:
Long Tasks บน Main Thread
Long Tasks คือ tasks ที่ใช้เวลามากกว่า 50 มิลลิวินาทีบน main thread ระหว่างที่ Long Task กำลังทำงาน เบราว์เซอร์ไม่สามารถตอบสนองต่อการโต้ตอบของผู้ใช้ได้เลย ซึ่งส่งผลโดยตรงต่อ Input Delay
Event Handlers ที่หนักเกินไป
ตัวอย่างเช่น event handlers ที่ filter รายการขนาดใหญ่ทุกครั้งที่ผู้ใช้พิมพ์ตัวอักษร หรือ re-render component ทั้งหมดเมื่อมีการเปลี่ยนแปลงเล็กน้อย พวกนี้เป็นปัญหาที่เจอกันบ่อยมาก
Layout Thrashing
อันนี้ร้ายกาจ เกิดเมื่อโค้ด JavaScript อ่านค่า layout (เช่น offsetHeight, getBoundingClientRect()) แล้วเขียนค่า style กลับไป สลับกันซ้ำ ๆ ในลูป ทำให้เบราว์เซอร์ต้องคำนวณ layout ใหม่ทุกรอบ
DOM ที่มีขนาดใหญ่เกินไป
เว็บไซต์ที่มี DOM nodes หลายพันตัวจะทำให้ทุกการอัปเดต style และ layout ช้าลงอย่างเห็นได้ชัด ส่งผลให้ Presentation Delay พุ่งสูง
Third-Party Scripts
พูดตรง ๆ — นี่คือตัวร้ายอันดับหนึ่งในหลาย ๆ เว็บไซต์ สคริปต์จากบุคคลที่สาม เช่น analytics, advertising, chat widgets และ social media embeds สามารถ บล็อก main thread ได้นานถึง 500-1,500 มิลลิวินาที ซึ่งเพียงพอที่จะทำให้ INP พุ่งเกินเกณฑ์ "แย่" ได้สบาย ๆ
เทคนิคการเพิ่มประสิทธิภาพ INP อย่างละเอียด
มาถึงส่วนที่ทุกคนรอคอยกัน: วิธีแก้ไขปัญหา INP ในทางปฏิบัติจริง ๆ เราจะครอบคลุมเทคนิคหลากหลาย ตั้งแต่การแบ่ง task ไปจนถึงการย้ายงานออกจาก main thread
การแบ่ง Long Tasks ด้วย scheduler.yield()
วิธีที่มีประสิทธิภาพที่สุดในการลด Input Delay คือการแบ่ง Long Tasks ออกเป็นชิ้นเล็ก ๆ เพื่อให้เบราว์เซอร์มีโอกาสตอบสนองต่อผู้ใช้ระหว่างแต่ละชิ้น ในปี 2026 scheduler.yield() เป็น API มาตรฐานที่รองรับข้ามเบราว์เซอร์แล้ว
ก่อนการเพิ่มประสิทธิภาพ (Long Task เดียวที่บล็อก main thread):
// Bad: One long task that blocks the main thread
function processLargeDataset(items) {
const results = [];
for (const item of items) {
// Heavy computation for each item
const processed = heavyComputation(item);
results.push(processed);
updateDOM(processed);
}
return results;
}
หลังการเพิ่มประสิทธิภาพด้วย scheduler.yield():
// Good: Break work into chunks, yielding between each
async function processLargeDataset(items) {
const results = [];
const CHUNK_SIZE = 5;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
for (const item of chunk) {
const processed = heavyComputation(item);
results.push(processed);
}
// Yield to the main thread between chunks
// This allows the browser to process pending user interactions
await scheduler.yield();
}
// Batch DOM updates after processing
updateDOMBatch(results);
return results;
}
สิ่งที่ scheduler.yield() ทำก็คือ "คืน" การควบคุมให้เบราว์เซอร์ชั่วคราว เพื่อให้มันจัดการงานสำคัญกว่า เช่น การตอบสนองต่อคลิกหรือการพิมพ์ หลังจากนั้นก็กลับมาทำงานต่อจากจุดที่หยุดไว้ ข้อดีสำคัญเมื่อเทียบกับ setTimeout(0) คือ task ที่ yield ไปจะถูกจัดลำดับความสำคัญไว้ด้านหน้าของ task queue ไม่ต้องรอ task อื่นทำก่อน
สำหรับเบราว์เซอร์รุ่นเก่าที่ยังไม่รองรับ คุณสามารถใช้ polyfill ได้:
// Polyfill for browsers without scheduler.yield()
async function yieldToMain() {
if ('scheduler' in globalThis && 'yield' in scheduler) {
return scheduler.yield();
}
// Fallback to setTimeout
return new Promise(resolve => setTimeout(resolve, 0));
}
// Usage with polyfill
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
doWork(items[i]);
// Yield every 5 items
if (i % 5 === 0) {
await yieldToMain();
}
}
}
การใช้ requestIdleCallback และ requestAnimationFrame
นอกจาก scheduler.yield() แล้ว ยังมี API อื่น ๆ ที่ช่วยจัดสรรงานได้เหมาะสม:
requestIdleCallback — เหมาะสำหรับงานที่ไม่เร่งด่วนและรอได้ เช่น ส่ง analytics data, preload ข้อมูล หรือ cleanup ทรัพยากร
// Use requestIdleCallback for non-urgent work
function sendAnalyticsData(data) {
if ('requestIdleCallback' in window) {
requestIdleCallback((deadline) => {
// deadline.timeRemaining() tells us how much free time we have
while (data.length > 0 && deadline.timeRemaining() > 5) {
const item = data.shift();
navigator.sendBeacon('/analytics', JSON.stringify(item));
}
// If there's still data left, schedule another idle callback
if (data.length > 0) {
requestIdleCallback(() => sendAnalyticsData(data));
}
}, { timeout: 3000 }); // Max wait 3 seconds
} else {
// Fallback: use setTimeout with a delay
setTimeout(() => {
data.forEach(item => {
navigator.sendBeacon('/analytics', JSON.stringify(item));
});
}, 2000);
}
}
requestAnimationFrame — เหมาะสำหรับงานที่เกี่ยวกับการอัปเดตภาพบนหน้าจอ เช่น animations หรืออัปเดต DOM ที่ต้องการความลื่นไหล
// Use requestAnimationFrame for visual updates
function animateProgressBar(progress) {
const bar = document.querySelector('.progress-bar');
requestAnimationFrame(() => {
bar.style.width = `${progress}%`;
bar.setAttribute('aria-valuenow', progress);
// Chain the next frame if animation is not complete
if (progress < 100) {
animateProgressBar(progress + 1);
}
});
}
// Batch DOM reads and writes using rAF to avoid layout thrashing
function updateElementPositions(elements) {
// Read phase: collect all measurements first
const measurements = elements.map(el => ({
el,
height: el.offsetHeight,
top: el.getBoundingClientRect().top
}));
// Write phase: apply all changes in a single rAF
requestAnimationFrame(() => {
measurements.forEach(({ el, height, top }) => {
el.style.transform = `translateY(${calculateNewPosition(height, top)}px)`;
});
});
}
การเพิ่มประสิทธิภาพ Event Handlers: Debouncing และ Throttling
Event handlers หลายตัวถูกเรียกบ่อยเกินความจำเป็น โดยเฉพาะ scroll, resize, input และ mousemove การใช้ debouncing และ throttling ช่วยลดจำนวนครั้งที่ handler ถูกเรียกได้มหาศาล
Debouncing จะรอจนกว่าผู้ใช้หยุดโต้ตอบแล้วค่อยทำงาน เหมาะมากสำหรับ search input ที่ต้องเรียก API:
// Debounce: Wait until user stops typing, then execute
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Usage: Search input handler
const searchInput = document.querySelector('#search');
const handleSearch = debounce(async (event) => {
const query = event.target.value.trim();
if (query.length < 2) return;
const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await results.json();
renderSearchResults(data);
}, 300); // Wait 300ms after last keystroke
searchInput.addEventListener('input', handleSearch);
Throttling จำกัดให้ handler ทำงานไม่เกินอัตราที่กำหนด เหมาะสำหรับ scroll หรือ resize events:
// Throttle: Execute at most once every specified interval
function throttle(func, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
// Usage: Scroll handler for lazy loading or infinite scroll
const handleScroll = throttle(() => {
const scrollPosition = window.scrollY + window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
if (documentHeight - scrollPosition < 500) {
loadMoreContent();
}
}, 200); // Run at most once every 200ms
window.addEventListener('scroll', handleScroll, { passive: true });
สังเกตว่าเราใช้ { passive: true } กับ scroll event listener ด้วยนะ การบอกเบราว์เซอร์ว่า handler นี้ไม่เรียก preventDefault() ช่วยให้เบราว์เซอร์ scroll ได้ลื่นไหลขึ้น เพราะไม่ต้องรอ handler ทำงานเสร็จก่อนถึงจะ scroll ได้
กลยุทธ์ Code Splitting
การโหลด JavaScript ทั้งหมดในครั้งเดียวเป็นสาเหตุหลักที่ทำให้ Long Tasks เกิดขึ้นตอนโหลดหน้า ส่งผลต่อ Input Delay ของการโต้ตอบแรก ๆ โดยตรง Code Splitting คือการแบ่ง JavaScript bundle ออกเป็นส่วนย่อยแล้วโหลดเฉพาะที่จำเป็น
// Dynamic import: Load code only when needed
document.querySelector('#open-modal').addEventListener('click', async () => {
// Show a lightweight loading indicator immediately
showSpinner();
// Load the modal module on demand
const { Modal } = await import('./components/Modal.js');
hideSpinner();
const modal = new Modal();
modal.open();
});
// Route-based code splitting with dynamic imports
const routes = {
'/dashboard': () => import('./pages/Dashboard.js'),
'/settings': () => import('./pages/Settings.js'),
'/reports': () => import('./pages/Reports.js'),
};
async function navigateTo(path) {
const loadPage = routes[path];
if (loadPage) {
const module = await loadPage();
module.default.render();
}
}
เคล็ดลับสำคัญ: การแสดง loading indicator ทันทีก่อนโหลดโมดูลช่วยให้ผู้ใช้รู้สึกว่าเว็บตอบสนองเร็วขึ้น แม้เวลาโดยรวมอาจไม่ต่างกัน แต่ visual feedback ทันทีสร้างความรู้สึกที่ดีขึ้นมาก
การใช้ Web Workers สำหรับการคำนวณหนัก
งานที่ใช้ CPU สูง เช่น ประมวลผลข้อมูลขนาดใหญ่ sort ข้อมูล หรือ encrypt ไม่ควรอยู่บน main thread เลย จริง ๆ นะ Web Workers ช่วยให้คุณย้ายงานพวกนี้ไปรันใน background thread แยกต่างหาก ทำให้ main thread ว่างสำหรับการตอบสนองต่อผู้ใช้
// main.js - Main thread
const worker = new Worker('/workers/data-processor.js');
document.querySelector('#process-btn').addEventListener('click', () => {
const data = getFormData();
// Show immediate feedback to the user
showProcessingState();
// Send heavy work to the Web Worker
worker.postMessage({ action: 'process', data });
});
// Receive results from the worker
worker.addEventListener('message', (event) => {
const { results, status } = event.data;
if (status === 'complete') {
hideProcessingState();
renderResults(results);
} else if (status === 'progress') {
updateProgressBar(event.data.percent);
}
});
// workers/data-processor.js - Worker thread (runs off main thread)
self.addEventListener('message', (event) => {
const { action, data } = event.data;
if (action === 'process') {
const results = [];
const total = data.length;
for (let i = 0; i < total; i++) {
// Heavy computation happens here, OFF the main thread
results.push(complexCalculation(data[i]));
// Report progress every 10%
if (i % Math.floor(total / 10) === 0) {
self.postMessage({
status: 'progress',
percent: Math.round((i / total) * 100)
});
}
}
self.postMessage({ status: 'complete', results });
}
});
function complexCalculation(item) {
// CPU-intensive work that would block the main thread
// e.g., data transformation, statistical analysis, etc.
return transformedItem;
}
การใช้ Partytown สำหรับ Third-Party Scripts
ดังที่กล่าวไว้ก่อนหน้านี้ third-party scripts บล็อก main thread ได้นาน 500-1,500ms Partytown คือไลบรารีที่ย้าย third-party scripts ไปรันใน Web Worker โดยอัตโนมัติ ทำให้ main thread ว่างสำหรับงานสำคัญ
<!-- Load Partytown -->
<script src="/~partytown/partytown.js"></script>
<!-- Third-party scripts with type="text/partytown" run in a Web Worker -->
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
</script>
แค่เปลี่ยน type="text/javascript" เป็น type="text/partytown" สคริปต์พวกนั้นจะถูกดักจับและรันใน Web Worker แทน main thread ผลลัพธ์คือ main thread เบาลงอย่างเห็นได้ชัด ทำให้ INP ดีขึ้นมาก โดยเฉพาะในหน้าที่มี third-party scripts เยอะ
แต่ต้องบอกตรง ๆ ว่า Partytown ก็มีข้อจำกัด: สคริปต์ที่ต้องเข้าถึง DOM โดยตรงหรือต้องการ synchronous response อาจทำงานไม่ถูกต้อง ดังนั้นควรทดสอบให้ดีก่อนนำไปใช้งานจริง
เทคนิคเฉพาะเฟรมเวิร์ก
แต่ละเฟรมเวิร์ก JavaScript ชั้นนำมี API เฉพาะที่ช่วยเรื่องความตอบสนอง มาดูกันทีละตัว:
React: useDeferredValue และ useTransition
React 18+ มี hooks สองตัวที่ช่วยจัดลำดับความสำคัญของการอัปเดต UI ได้ดีมาก:
import { useState, useDeferredValue, useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
// useDeferredValue: Show stale results while computing new ones
const deferredQuery = useDeferredValue(query);
function handleChange(e) {
// The input value updates immediately (high priority)
setQuery(e.target.value);
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <span>Loading...</span>}
{/* This uses the deferred value, so it can lag behind
the input without blocking the UI */}
<ResultsList query={deferredQuery} />
</div>
);
}
// useTransition example: Navigating between tabs
function TabContainer() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
// Mark this state update as a transition (low priority)
// The UI remains responsive while the new tab renders
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<nav>
<button onClick={() => selectTab('home')}>Home</button>
<button onClick={() => selectTab('reports')}>Reports</button>
<button onClick={() => selectTab('settings')}>Settings</button>
</nav>
<div className={isPending ? 'opacity-50' : ''}>
<TabContent tab={tab} />
</div>
</div>
);
}
useTransition บอก React ว่าการอัปเดต state นี้เป็น "transition" ที่มีความสำคัญต่ำ ถ้าผู้ใช้โต้ตอบอีกครั้งระหว่างที่ transition กำลัง render React ก็จะหยุด render ของ transition เพื่อตอบสนองต่อการโต้ตอบก่อน ช่วย INP ได้โดยตรงเลย
Vue: v-memo Directive
Vue 3 มี v-memo directive ที่ช่วย memoize ส่วนของ template tree เพื่อหลีกเลี่ยง re-render ที่ไม่จำเป็น:
<!-- Vue 3: v-memo prevents re-renders when dependencies haven't changed -->
<template>
<div class="product-list">
<!-- Only re-render items when their specific data changes -->
<div
v-for="item in products"
:key="item.id"
v-memo="[item.price, item.stock, item.id === selectedId]"
>
<ProductCard
:product="item"
:isSelected="item.id === selectedId"
@click="selectProduct(item.id)"
/>
</div>
</div>
</template>
<script setup>
import { ref, shallowRef } from 'vue';
// Use shallowRef for large datasets to avoid deep reactivity overhead
const products = shallowRef([]);
const selectedId = ref(null);
function selectProduct(id) {
selectedId.value = id;
// Only the selected and previously selected items will re-render
}
</script>
v-memo มีประสิทธิภาพมากกับรายการขนาดใหญ่ แทนที่จะ re-render ทุกรายการเมื่อมีการเปลี่ยนแปลง จะ re-render เฉพาะรายการที่ dependency เปลี่ยนจริง ๆ เท่านั้น
Angular: @defer Block
Angular 17+ มี @defer block ที่ทำให้โหลด component แบบ lazy ได้ง่ายขึ้นมาก:
<!-- Angular @defer: Lazy load components based on conditions -->
<!-- Load heavy chart component only when it enters the viewport -->
@defer (on viewport) {
<app-analytics-chart [data]="chartData" />
} @placeholder {
<div class="chart-skeleton">Loading chart...</div>
} @loading (minimum 500ms) {
<app-loading-spinner />
}
<!-- Load comment section only when user interacts with it -->
@defer (on interaction) {
<app-comment-section [postId]="post.id" />
} @placeholder {
<button>Show Comments ({{ post.commentCount }})</button>
}
<!-- Load recommendations after initial content is ready -->
@defer (on idle) {
<app-recommendations [userId]="currentUser.id" />
} @placeholder {
<div class="recommendations-placeholder"></div>
}
@defer ช่วย INP ได้สองทาง: ลดขนาด JavaScript ที่ต้องโหลดตอนแรก (ลด Long Tasks จากการ parse) และลดขนาด DOM เริ่มต้น (ลด Presentation Delay) ถือว่าเป็นฟีเจอร์ที่คุ้มค่ามาก
การลดขนาด DOM และเพิ่มประสิทธิภาพ Rendering
DOM ขนาดใหญ่เป็นผู้ร้ายที่มักถูกมองข้ามในเรื่อง INP (จากประสบการณ์ของผู้เขียน นี่เป็นสิ่งที่หลายทีมไม่ได้นึกถึงเลยจนกว่าจะ debug ดู) เมื่อ DOM มีโหนดเยอะ ทุกครั้งที่เบราว์เซอร์ต้องคำนวณ style, layout หรือ paint จะช้าลงตามสัดส่วน
Virtualization สำหรับรายการยาว — แทนที่จะ render สมาชิกทั้งหมด ให้ render เฉพาะส่วนที่มองเห็นได้บนหน้าจอ:
// Simple virtual scrolling concept
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;
this.container.style.overflow = 'auto';
this.content = document.createElement('div');
this.content.style.height = `${items.length * itemHeight}px`;
this.content.style.position = 'relative';
this.container.appendChild(this.content);
this.container.addEventListener('scroll',
throttle(() => this.render(), 16),
{ passive: true }
);
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(
startIndex + this.visibleCount,
this.items.length
);
// Clear and re-render only visible items
this.content.innerHTML = '';
for (let i = startIndex; i < endIndex; i++) {
const el = this.createItemElement(this.items[i]);
el.style.position = 'absolute';
el.style.top = `${i * this.itemHeight}px`;
this.content.appendChild(el);
}
}
}
content-visibility: auto เป็น CSS property ที่บอกเบราว์เซอร์ให้ข้ามการ render สำหรับ elements ที่อยู่นอกหน้าจอ แค่เพิ่ม CSS ไม่กี่บรรทัดก็ช่วยได้เยอะ:
/* CSS: Skip rendering for off-screen sections */
.content-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px; /* Estimated height for layout */
}
การวัดผล INP ด้วยเครื่องมือต่าง ๆ
การเพิ่มประสิทธิภาพที่ไม่วัดผลก็เหมือนขับรถโดยไม่มีมาตรวัดความเร็ว คุณต้องมีเครื่องมือที่เหมาะสม
Chrome User Experience Report (CrUX)
CrUX เป็นชุดข้อมูลสาธารณะที่รวบรวมข้อมูลประสิทธิภาพจากผู้ใช้ Chrome จริงทั่วโลก และเป็นแหล่งข้อมูลเดียวกับที่ Google ใช้ประเมิน Core Web Vitals สำหรับการจัดอันดับ คุณเข้าถึง CrUX ได้ผ่าน PageSpeed Insights, BigQuery หรือ CrUX API
Chrome DevTools: Performance Panel
DevTools Performance panel ให้รายละเอียดระดับมิลลิวินาทีของทุกสิ่งที่เกิดขึ้นระหว่างการโต้ตอบ:
- เปิด "Web Vitals" track เพื่อเห็นการโต้ตอบแต่ละครั้งที่ถูกวัดเป็น INP
- เห็น Input Delay, Processing Time และ Presentation Delay แยกกัน
- ระบุ Long Tasks ที่ก่อให้เกิด Input Delay
- ตรวจสอบว่า event handler ไหนใช้เวลานานที่สุด
Long Animation Frames API (LoAF)
LoAF API เป็นเครื่องมือใหม่ที่ทรงพลังมากสำหรับการวิเคราะห์ INP ในสภาพแวดล้อมจริง มันให้ข้อมูลละเอียดกว่า Long Tasks API อีก รวมถึง script attribution ที่บอกได้เลยว่าสคริปต์ไหนเป็นตัวก่อปัญหา
// Using the Long Animation Frames (LoAF) API to diagnose INP issues
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Only look at long animation frames (>50ms)
if (entry.duration > 50) {
console.group(`Long Animation Frame: ${entry.duration.toFixed(0)}ms`);
console.log('Start time:', entry.startTime.toFixed(0));
console.log('Duration:', entry.duration.toFixed(0));
console.log('Block duration:', entry.blockingDuration.toFixed(0));
// Identify which scripts contributed to this long frame
for (const script of entry.scripts) {
console.log('Script:', {
sourceURL: script.sourceURL,
sourceFunctionName: script.sourceFunctionName,
invokerType: script.invokerType,
invoker: script.invoker,
duration: script.duration.toFixed(0),
executionStart: script.executionStart.toFixed(0),
windowAttribution: script.windowAttribution
});
}
console.groupEnd();
}
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
// Correlating LoAF with INP for field debugging
const inpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Get the INP interaction
if (entry.interactionId > 0) {
const inpData = {
metric: 'INP',
value: entry.duration,
inputDelay: entry.processingStart - entry.startTime,
processingTime: entry.processingEnd - entry.processingStart,
presentationDelay: entry.startTime + entry.duration - entry.processingEnd,
target: entry.target?.tagName,
type: entry.name
};
// Send to your analytics endpoint
if (entry.duration > 200) {
navigator.sendBeacon('/api/performance', JSON.stringify(inpData));
}
}
}
});
inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 16 });
LoAF API ช่วยให้คุณระบุได้อย่างแม่นยำว่าสคริปต์ไหน ฟังก์ชันไหน และ event handler ไหนเป็นสาเหตุ ข้อมูลแบบนี้จำเป็นมากสำหรับการแก้ปัญหาในโปรดักชัน
เครื่องมือเสริมอื่น ๆ
- web-vitals JavaScript library: ไลบรารีจาก Google ที่วัด Core Web Vitals ทั้งหมดรวมถึง INP พร้อม attribution data ใช้ง่ายและเชื่อถือได้
- Lighthouse: แม้จะเป็น synthetic testing ที่วัด INP โดยตรงไม่ได้ แต่ช่วยระบุ Long Tasks และปัญหาอื่น ๆ ที่ส่งผลต่อ INP ได้
- Real User Monitoring (RUM) services: บริการเช่น SpeedCurve, Datadog RUM หรือ New Relic Browser ให้การติดตาม INP แบบ real-time พร้อม dashboards และ alerting
กรณีศึกษาและกลยุทธ์การจัดลำดับความสำคัญ
การเพิ่มประสิทธิภาพ INP ในโลกจริงต้องทำอย่างเป็นระบบ ไม่ใช่แก้ทีละจุดแบบมั่ว ๆ ต่อไปนี้คือแนวทางที่ใช้ได้จริง:
ขั้นตอนที่ 1: วิเคราะห์ข้อมูลภาคสนาม
เริ่มจากดูข้อมูล CrUX หรือ RUM ของคุณเพื่อตอบคำถามเหล่านี้:
- หน้าไหนมี INP แย่ที่สุด?
- การโต้ตอบประเภทไหนช้าที่สุด (คลิก, แตะ, พิมพ์)?
- ปัญหาเกิดเฉพาะบนมือถือหรือ desktop ด้วย?
- INP แย่เกิดจากเฟสไหน (Input Delay, Processing Time หรือ Presentation Delay)?
ขั้นตอนที่ 2: จัดลำดับความสำคัญตาม Impact
ไม่ใช่ทุกหน้าจะมีความสำคัญเท่ากัน ให้จัดลำดับตามสูตร:
Priority = ผลกระทบ INP x ปริมาณทราฟฟิก x มูลค่าทางธุรกิจ
ยกตัวอย่างเช่น หน้า product detail ที่มี INP 350ms และทราฟฟิก 100,000 visits/เดือน ย่อมสำคัญกว่าหน้า blog ที่มี INP 400ms แต่ทราฟฟิกแค่ 1,000 visits/เดือน
ขั้นตอนที่ 3: แก้ไขตามลำดับผลตอบแทนสูงสุด
จากประสบการณ์ของเราในการทำ performance optimization มาหลายโปรเจกต์ ลำดับที่มักให้ผลตอบแทนสูงสุดคือ:
- ย้าย third-party scripts ออกจาก main thread — มักให้ผลดีที่สุดเพราะลด Input Delay ได้ 500-1,500ms ในทันที
- แบ่ง Long Tasks ด้วย scheduler.yield() — ลด Input Delay สำหรับ first-party code
- เพิ่มประสิทธิภาพ event handlers ที่ช้าที่สุด — ลด Processing Time โดยตรง
- ลดขนาด DOM และใช้ content-visibility — ลด Presentation Delay
- ทำ code splitting — ลดขนาด JavaScript ที่ต้องโหลดและ parse ตอนแรก
ตัวอย่างกรณีศึกษา: เว็บไซต์ E-commerce
สมมติเว็บไซต์ e-commerce ที่มี INP อยู่ที่ 450ms บนมือถือ (อยู่ในเกณฑ์ "ต้องปรับปรุง") จากการวิเคราะห์ด้วย LoAF API พบว่า:
- Input Delay: 180ms — เกิดจาก Google Tag Manager และ analytics scripts
- Processing Time: 170ms — เกิดจาก product filter ที่ re-render รายการ 2,000 ชิ้น
- Presentation Delay: 100ms — เกิดจาก DOM ขนาด 3,500 โหนด
แผนการแก้ไขทีละสัปดาห์:
- สัปดาห์ที่ 1: ย้าย GTM และ analytics scripts ไปใช้ Partytown หรือ defer ด้วย
requestIdleCallback— ลด Input Delay จาก 180ms เหลือ 30ms - สัปดาห์ที่ 2: ใช้ virtualization สำหรับ product list และเพิ่ม
v-memo(ถ้าใช้ Vue) — ลด Processing Time จาก 170ms เหลือ 40ms และลด DOM nodes จาก 3,500 เหลือ 800 - สัปดาห์ที่ 3: เพิ่ม debouncing สำหรับ filter input และใช้
scheduler.yield()ในการประมวลผล filter — ลด Processing Time เพิ่มเติม
ผลลัพธ์? INP ลดจาก 450ms เหลือประมาณ 120ms ซึ่งอยู่ในเกณฑ์ "ดี" เป็นที่เรียบร้อย ค่อนข้างน่าประทับใจทีเดียว
เทคนิคขั้นสูง: การหลีกเลี่ยง Layout Thrashing
Layout thrashing เป็นปัญหาที่เจอบ่อยแต่แก้ได้ง่ายเมื่อรู้วิธี หลักการสำคัญง่าย ๆ เลย: อ่านค่า layout ทั้งหมดก่อน แล้วค่อยเขียนทีหลัง อย่าสลับกัน
// BAD: Layout thrashing - forces multiple reflows
function resizeElements(elements) {
elements.forEach(el => {
// Read (forces layout calculation)
const width = el.offsetWidth;
// Write (invalidates layout)
el.style.width = (width * 1.1) + 'px';
// Next iteration reads again, forcing ANOTHER layout calculation
});
}
// GOOD: Batch reads then batch writes
function resizeElements(elements) {
// Phase 1: Read all measurements
const widths = elements.map(el => el.offsetWidth);
// Phase 2: Write all changes (only one reflow at the end)
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.width = (widths[i] * 1.1) + 'px';
});
});
}
ถ้าจำเป็นต้องอ่านและเขียนสลับกันจริง ๆ ให้ใช้ requestAnimationFrame เพื่อจัดกลุ่มการเขียนไว้ในเฟรมถัดไป หรือลองใช้ไลบรารี fastdom ที่จัดการเรื่องนี้ให้อัตโนมัติ (ประหยัดเวลาไปเยอะ)
กลยุทธ์สำหรับมือถือ: ทำไม INP บนมือถือถึงสำคัญกว่า
ด้วยทราฟฟิกมือถือที่คิดเป็น 58% ของเว็บทั้งหมดในปี 2026 และอุปกรณ์มือถือที่มี CPU กับหน่วยความจำน้อยกว่า desktop หลายเท่า INP บนมือถือจึงมักจะแย่กว่า desktop อย่างเห็นได้ชัด
เทคนิคที่ควรให้ความสำคัญเป็นพิเศษสำหรับมือถือ:
- ทดสอบด้วย CPU throttling: ใน DevTools ให้เปิด CPU throttling 4x-6x เพื่อจำลองอุปกรณ์มือถือระดับกลาง อย่าทดสอบแค่บน MacBook Pro ของคุณ!
- ลด JavaScript ให้เหลือน้อยที่สุด: ทุก KB ของ JavaScript ที่ต้อง parse และ compile มีต้นทุนสูงกว่าบนมือถือมาก
- ใช้
passive: trueกับ touch และ scroll events ทุกตัวที่ไม่ต้องการpreventDefault() - หลีกเลี่ยง hover-dependent interactions: มือถือไม่มี hover ให้ออกแบบ UI ที่ตอบสนองต่อ tap โดยตรง
- ใช้
touch-actionCSS: บอกเบราว์เซอร์ล่วงหน้าว่า element จะจัดการ touch gesture อย่างไร เพื่อลด delay
/* Eliminate 300ms tap delay on interactive elements */
button, a, [role="button"] {
touch-action: manipulation;
}
/* For elements that only need vertical scroll */
.scrollable-list {
touch-action: pan-y;
}
การจัดการ Third-Party Scripts อย่างเป็นระบบ
Third-party scripts เป็นปัจจัยที่ควบคุมยากที่สุดแต่มีผลกระทบมากที่สุดต่อ INP นอกเหนือจาก Partytown ยังมีกลยุทธ์อื่น ๆ ที่น่าสนใจ:
// Strategy 1: Lazy load third-party scripts after user interaction
let chatWidgetLoaded = false;
document.querySelector('#chat-button').addEventListener('click', () => {
if (!chatWidgetLoaded) {
const script = document.createElement('script');
script.src = 'https://chat-provider.com/widget.js';
script.async = true;
document.head.appendChild(script);
chatWidgetLoaded = true;
}
});
// Strategy 2: Load non-critical scripts during idle time
function loadNonCriticalScripts() {
const scripts = [
'https://analytics.example.com/tracker.js',
'https://social.example.com/share-buttons.js'
];
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
scripts.forEach(src => {
const script = document.createElement('script');
script.src = src;
script.async = true;
document.head.appendChild(script);
});
}, { timeout: 5000 });
} else {
// Fallback: load after 3 seconds
setTimeout(() => {
scripts.forEach(src => {
const script = document.createElement('script');
script.src = src;
script.async = true;
document.head.appendChild(script);
});
}, 3000);
}
}
// Trigger after the page is fully loaded
window.addEventListener('load', loadNonCriticalScripts);
// Strategy 3: Use a facade pattern for heavy embeds
class YouTubeFacade extends HTMLElement {
connectedCallback() {
const videoId = this.getAttribute('video-id');
this.innerHTML = `
<button
style="background: url(https://i.ytimg.com/vi/${videoId}/hqdefault.jpg) center/cover;
width: 100%; aspect-ratio: 16/9; border: none; cursor: pointer; position: relative;"
aria-label="Play video"
>
<svg style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)" width="68" height="48" viewBox="0 0 68 48">
<path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="red"/>
<path d="M45 24L27 14v20" fill="white"/>
</svg>
</button>
`;
// Only load the full YouTube iframe when the user clicks
this.querySelector('button').addEventListener('click', () => {
this.innerHTML = `
<iframe
width="100%"
style="aspect-ratio:16/9"
src="https://www.youtube.com/embed/${videoId}?autoplay=1"
frameborder="0"
allow="autoplay; encrypted-media"
allowfullscreen
></iframe>
`;
}, { once: true });
}
}
customElements.define('youtube-facade', YouTubeFacade);
Facade pattern มีประสิทธิภาพสูงเป็นพิเศษสำหรับ YouTube, Google Maps, social media embeds และ live chat widgets ที่มักโหลด JavaScript หลายร้อย KB ลองเปรียบเทียบดู: รูปภาพ thumbnail ขนาดเล็กกับ iframe ที่โหลดทั้งหน้า YouTube player — ความแตกต่างชัดเจนมาก
สรุปและ Checklist สำหรับการเพิ่มประสิทธิภาพ INP
INP ซับซ้อนจริง แต่ก็แก้ไขได้อย่างเป็นระบบ นี่คือ checklist ที่ครอบคลุมสำหรับปี 2026:
การวิเคราะห์และวัดผล
- ตรวจสอบคะแนน INP ปัจจุบันจาก CrUX หรือ RUM (เป้าหมาย: น้อยกว่า 200ms)
- ใช้ LoAF API เพื่อระบุสาเหตุที่แท้จริง
- แยกวิเคราะห์ว่าปัญหาอยู่ที่เฟสไหน: Input Delay, Processing Time หรือ Presentation Delay
- ทดสอบบนอุปกรณ์มือถือจริงหรือใช้ CPU throttling 4x-6x
- ตั้ง monitoring และ alerting สำหรับ INP regression
การลด Input Delay
- แบ่ง Long Tasks ด้วย
scheduler.yield() - ย้าย third-party scripts ออกจาก main thread (Partytown, lazy loading, facade pattern)
- ทำ code splitting เพื่อลด JavaScript ที่ต้อง parse ตอนโหลดหน้า
- ใช้
requestIdleCallbackสำหรับงานไม่เร่งด่วน - โหลดสคริปต์ที่ไม่จำเป็นด้วย
asyncหรือdefer
การลด Processing Time
- ใช้ debouncing และ throttling สำหรับ event handlers ที่ถูกเรียกบ่อย
- ย้ายการคำนวณหนักไปยัง Web Workers
- ใช้ framework-specific optimizations:
useTransition/useDeferredValue(React),v-memo(Vue),@defer(Angular) - หลีกเลี่ยง synchronous DOM manipulation ที่ไม่จำเป็นใน event handlers
- ใช้
passive: trueกับ scroll และ touch event listeners
การลด Presentation Delay
- ลดขนาด DOM ให้เล็กที่สุด (เป้าหมาย: น้อยกว่า 1,500 โหนด)
- ใช้ virtualization สำหรับรายการยาว
- ใช้
content-visibility: autoสำหรับเนื้อหาที่อยู่นอกหน้าจอ - หลีกเลี่ยง layout thrashing: อ่านค่า layout ทั้งหมดก่อน แล้วค่อยเขียน
- ใช้
transformและopacityสำหรับ animations แทน properties ที่ trigger layout
กลยุทธ์ระยะยาว
- ตรวจสอบ third-party scripts เป็นประจำและลบสคริปต์ที่ไม่จำเป็นออก
- ตั้ง performance budget สำหรับ JavaScript (เป้าหมาย: น้อยกว่า 300KB compressed สำหรับ first-party code)
- ทำ performance review เป็นส่วนหนึ่งของ code review process
- ติดตาม INP แยกตาม device type, connection speed และ geographic location
- พิจารณาใช้ Speculation Rules API เพื่อ prerender หน้าถัดไป
บทส่งท้าย
INP ไม่ใช่แค่ตัวชี้วัดทางเทคนิคอีกตัว มันวัดสิ่งที่ผู้ใช้ รู้สึก จริง ๆ เมื่อโต้ตอบกับเว็บไซต์ เว็บไซต์ที่มี INP ดีจะให้ความรู้สึก "เร็ว" และ "ลื่นไหล" ส่วนเว็บที่ INP แย่ก็จะ "หนืด" และ "ไม่ตอบสนอง" ไม่ว่าจะโหลดเร็วแค่ไหนก็ตาม
ในปี 2026 ด้วย 43% ของเว็บไซต์ที่ยังไม่ผ่านเกณฑ์ การเพิ่มประสิทธิภาพ INP คือ โอกาสทางการแข่งขันที่ชัดเจน เว็บไซต์ที่ผ่านเกณฑ์ Core Web Vitals ทั้งหมดจะได้เปรียบทั้งในเรื่องประสบการณ์ผู้ใช้ อัตราการตีกลับที่ต่ำลง 24% และอันดับการค้นหาที่ดีขึ้น
กุญแจสำคัญคือทำอย่างเป็นระบบ: วัดผลก่อน ระบุปัญหา จัดลำดับความสำคัญ แก้ไขทีละส่วน แล้ววัดผลซ้ำ อย่าพยายามแก้ทุกอย่างพร้อมกัน เริ่มจากสิ่งที่ให้ผลตอบแทนสูงสุดก่อน ไม่ว่าจะเป็นการย้าย third-party scripts ออกจาก main thread หรือแบ่ง Long Tasks ด้วย scheduler.yield()
ทุกมิลลิวินาทีที่คุณลดลงจาก INP คือมิลลิวินาทีที่ผู้ใช้ได้กลับคืนมา และในโลกที่ความอดทนของผู้ใช้วัดเป็นเศษส่วนของวินาที นั่นแหละที่สร้างความแตกต่าง