Core Web Vitals ปี 2026: LCP INP CLS วัดยังไง หาตัวการ และแก้ทีละตัว
สามตัวเลขที่ Google ใช้วัดว่าเว็บเร็วและนิ่งพอไหม เกณฑ์ 2.5 วินาที 200 ms และ 0.1 ที่ p75 ข้อมูลจริงต่างจากข้อมูลแล็บยังไง และวิธีแก้ที่ได้ผลของแต่ละตัว
- เกณฑ์ผ่านคือ LCP ไม่เกิน 2.5 วินาที INP ไม่เกิน 200 ms CLS ไม่เกิน 0.1 วัดที่ percentile 75 แยกมือถือกับ desktop
- INP แทน FID ตั้งแต่ 12 มีนาคม 2024 และวัดทุกการคลิก แตะ กดแป้น ไม่ใช่แค่ครั้งแรก
- ตัวที่ Google ใช้คือข้อมูลจากผู้ใช้จริง (CrUX 28 วัน) Lighthouse ช่วยหาสาเหตุแต่ไม่ใช่คะแนนที่นับ
- ค่าเฉลี่ยหลอกตา ต้องแก้หางช้า ๆ ของผู้ใช้มือถือ ไม่ใช่ทำเครื่องตัวเองให้เร็ว
Core Web Vitals คือตัวเลขสามตัวที่ Google ใช้บอกว่าหน้าเว็บโหลดเร็วไหม (LCP) ตอบสนองไวไหม (INP) และนิ่งไหม (CLS) จากประสบการณ์ของผู้ใช้จริง มันเป็นส่วนหนึ่งของสัญญาณ page experience ในการค้นหา แต่เหตุผลที่สำคัญกว่าคือ ตัวเลขพวกนี้ตรงกับสิ่งที่คนรู้สึกเวลาเปิดเว็บบนมือถือ: รอนาน กดแล้วนิ่ง หรือกำลังจะกดแล้วปุ่มเลื่อนหนี
ตรวจข้อเท็จจริงกับเอกสาร web.dev, Chrome for Developers และ PageSpeed Insights ณ ตุลาคม 2026 โค้ดตรวจกับซอร์สของ web-vitals 6.2.3
สามตัวเลข และเกณฑ์ ณ ตุลาคม 2026
| ตัวชี้วัด | วัดอะไร | ดี | ต้องปรับ | แย่ |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | เวลาที่ภาพหรือข้อความก้อนใหญ่สุดในจอแรกแสดงเสร็จ | ≤ 2.5 วินาที | 2.5-4 วินาที | > 4 วินาที |
| INP (Interaction to Next Paint) | เวลาจากคลิก/แตะ/กดแป้น จนจอวาดผลลัพธ์ | ≤ 200 ms | 200-500 ms | > 500 ms |
| CLS (Cumulative Layout Shift) | ของในหน้าเลื่อนเองโดยผู้ใช้ไม่ได้สั่ง มากแค่ไหน | ≤ 0.1 | 0.1-0.25 | > 0.25 |
หน้าจะ “ผ่าน” เมื่อทั้งสามตัวอยู่ในช่องดี ที่ percentile 75 ของการเปิดหน้าทั้งหมด แยกมือถือกับ desktop คือ 3 ใน 4 ของการเปิดหน้าต้องได้ค่าดี
INP แทน FID ตั้งแต่ 12 มีนาคม 2024 FID วัดแค่ความหน่วงก่อนเริ่มทำงานของการกดครั้งแรก INP วัดทุกการคลิก แตะ และกดแป้นตลอดการอยู่ในหน้า แล้วรายงานครั้งที่แย่สุด (หน้าที่มีการโต้ตอบเยอะ จะตัดค่าสูงสุดทิ้งหนึ่งค่าทุก 50 ครั้ง กันค่าหลุดครั้งเดียว) การ scroll, hover และ zoom ไม่นับ
INP หนึ่งครั้งแบ่งได้สามช่วง
- input delay รอจน event handler ได้เริ่ม (main thread ยังติดงานอื่น)
- processing duration เวลาที่ handler รัน
- presentation delay จาก handler จบ จนเบราว์เซอร์วาดเฟรมถัดไป
ข้อมูลจริง กับ ข้อมูลแล็บ
นี่คือจุดที่ทีมสับสนบ่อยที่สุด
| ข้อมูลจริง (field) | ข้อมูลแล็บ (lab) | |
|---|---|---|
| มาจาก | CrUX: Chrome ของผู้ใช้จริงที่เปิดส่งสถิติ | Lighthouse รันบนเครื่องจำลอง |
| ช่วงเวลา | ย้อนหลัง 28 วัน อัปเดตทุกวัน | ณ วินาทีที่กดรัน |
| มี INP ไหม | มี | ไม่มี เพราะไม่มีใครคลิก ใช้ TBT (Total Blocking Time) ดู main thread ที่ติดแทน |
| Google ใช้ตัดสิน | ใช่ | ไม่ |
| ใช้ทำอะไร | รู้ว่ามีปัญหาจริงไหม หน้าไหน | หาสาเหตุ ทดลองแก้ก่อน deploy |
ข้อจำกัดของ CrUX ที่ควรรู้: นับเฉพาะ Chrome บน desktop และ Android ที่ผู้ใช้เปิดส่งสถิติและ sync ประวัติ ไม่รวม Chrome บน iOS, Safari, Firefox และ Edge และหน้าต้องมีคนเข้าถึงเกณฑ์ขั้นต่ำ (Google ไม่เปิดเผยตัวเลข) เว็บเล็กจึงมักเห็นแค่ข้อมูลระดับ origin หรือไม่มีเลย
Lighthouse 100 ไม่ได้แปลว่าผ่าน ตัวที่นับคือข้อมูลจริงที่ p75 บนมือถือของคนอื่น
ตัวอย่างคำนวณ: ค่าเฉลี่ยผ่าน แต่ p75 ไม่ผ่าน
สมมติเก็บ LCP มือถือได้ 20 ครั้ง (ms) เรียงจากน้อยไปมาก
1200 1300 1400 1500 1600 1700 1800 1900 2000 2100
2200 2300 2400 2500 2600 3100 3400 3900 4600 5200
- ค่าเฉลี่ย ผลรวม = 16,500 + 12,000 + 20,200 = 48,700 หาร 20 = 2,435 ms ดูเหมือนผ่าน
- median ค่าที่ 10 และ 11 คือ 2,100 กับ 2,200 = 2,150 ms ก็ผ่าน
- p75 (แบบ nearest-rank ง่าย ๆ) 0.75 × 20 = ลำดับที่ 15 = 2,600 ms ไม่ผ่าน
ตอนนี้มี 14 ครั้งที่ ≤ 2,500 ต้องได้ 15 จาก 20 (75%) ถึงจะผ่าน แปลว่าต้องดึงหางช้าอย่างน้อยหนึ่งครั้งลงมา หางห้าค่าบนสุดคือคนมือถือช้า เน็ตช้า ทำให้เครื่องเราเร็วขึ้นไม่ช่วยพวกเขาเลย (CrUX คำนวณ p75 จาก histogram ไม่ใช่วิธีนี้เป๊ะ แต่หลักการเหมือนกัน)
ต่อด้วยการแยก LCP ครั้งที่ 2,600 ms ออกเป็นสี่ส่วนตามที่ web.dev แนะนำ (ตัวเลขสมมติเพื่อให้เห็นภาพ)
| ส่วน | เวลา | สัดส่วนที่ web.dev แนะนำ |
|---|---|---|
| TTFB (รอ HTML ไบต์แรก) | 600 ms | ~40% |
| resource load delay (กว่าจะเริ่มโหลดรูป) | 900 ms | < 10% |
| resource load duration (โหลดรูป) | 900 ms | ~40% |
| element render delay (โหลดเสร็จแล้วรอวาด) | 200 ms | < 10% |
ช่อง load delay กิน 35% ทั้งที่ควรใกล้ศูนย์ แสดงว่าเบราว์เซอร์เจอรูปช้า ถ้า preload ทำให้เหลือ 150 ms: 600 + 150 + 900 + 200 = 1,850 ms ผ่านโดยไม่ต้องย่อรูปเลยสักไบต์
หาตัวการ: PageSpeed Insights, CrUX, web-vitals
- PageSpeed Insights (pagespeed.web.dev) ใส่ URL ส่วนบนคือข้อมูลจริงจาก CrUX ส่วนล่างคือ Lighthouse ดูส่วนบนก่อนว่าตัวไหนแดง แล้วใช้ส่วนล่างหาสาเหตุ
- Search Console > Core Web Vitals จับกลุ่ม URL ที่มีปัญหาเดียวกัน ดีกว่าไล่ทีละหน้า
- เก็บเองด้วยไลบรารี web-vitals ได้ข้อมูลทุกเบราว์เซอร์ที่รองรับ API (ไม่ใช่แค่ Chrome) และบอกได้ว่า element ไหน เป็นตัวการ
npm install web-vitals
// ใช้ build attribution เพื่อรู้ว่าตัวการคือ element ไหน
import { onCLS, onINP, onLCP } from 'web-vitals/attribution';
function send(metric) {
const a = metric.attribution;
const body = JSON.stringify({
name: metric.name, // 'LCP' | 'INP' | 'CLS'
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
page: location.pathname,
target: a.target ?? a.interactionTarget ?? a.largestShiftTarget,
// แยกส่วนให้รู้ว่าช้าเพราะอะไร
lcp: metric.name === 'LCP' && [a.timeToFirstByte, a.resourceLoadDelay,
a.resourceLoadDuration, a.elementRenderDelay],
inp: metric.name === 'INP' && [a.inputDelay, a.processingDuration, a.presentationDelay],
});
(navigator.sendBeacon && navigator.sendBeacon('/vitals', body)) ||
fetch('/vitals', { body, method: 'POST', keepalive: true });
}
onLCP(send);
onINP(send);
onCLS(send);
ฝั่งเซิร์ฟเวอร์เก็บลงตารางแล้ว query p75 ต่อหน้าต่อวัน ถ้าใช้ SQLite อยู่แล้ว ตารางเดียวก็พอ ค่า target เป็น CSS selector เช่น main > img.hero ซึ่งตอบได้เลยว่าต้องแก้ตรงไหน
แก้ LCP: ให้เบราว์เซอร์เจอรูปเร็ว และโหลดก่อน
- ใส่รูป LCP เป็น
<img>ใน HTML ตรง ๆ ถ้ารูปมาจาก CSS background หรือ JavaScript เบราว์เซอร์จะเจอช้า ถ้าเลี่ยงไม่ได้ ให้ preload fetchpriority="high"บนรูป LCP ใช้แค่ 1-2 รูป ใส่ทุกรูปเท่ากับไม่ได้ใส่ (รองรับ Chrome/Edge 101+, Firefox 132+, Safari 17.2+)- ห้าม
loading="lazy"บนรูป LCP เด็ดขาด lazy รอให้ layout ยืนยันว่ารูปอยู่ในจอก่อนค่อยโหลด เพิ่ม load delay ตรง ๆ - ลด TTFB HTML ที่ช้ากิน LCP ทุกครั้ง cache หน้า หรือตัดงานหนักออกจากทางร้อน
- ย่อรูปให้พอดีจอ ลด load duration ดูบทความรูป responsive
<!-- รูปอยู่ใน HTML: บอกความสำคัญตรง ๆ -->
<img src="/hero.webp" width="1200" height="675" alt="..." fetchpriority="high">
<!-- รูปอยู่ใน CSS/JS: preload ใน <head> ให้เจอตั้งแต่ HTML -->
<link rel="preload" as="image" href="/hero.webp" type="image/webp" fetchpriority="high">
แก้ INP: อย่าให้ main thread ติดนาน
งานใดที่ใช้ main thread เกิน 50 ms นับเป็น long task ระหว่างนั้นเบราว์เซอร์ตอบการกดไม่ได้ หลักคือ ทำส่วนที่ผู้ใช้เห็นก่อน แล้ว yield ให้เบราว์เซอร์วาดจอ ค่อยทำส่วนที่เหลือ
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}
button.addEventListener('click', async () => {
showSpinner(); // ผู้ใช้เห็นทันที
updateCart();
await yieldToMain(); // ให้เบราว์เซอร์วาดก่อน
saveToServer(); // งานที่ไม่ต้องเห็น
sendAnalytics();
});
scheduler.yield() ณ ตุลาคม 2026 มีใน Chrome/Edge 129+ และ Firefox 142+ ยังไม่มีใน Safari จึงต้องมี fallback setTimeout แบบข้างบนเสมอ
ตัวอย่าง (สมมติ): คลิกแล้ว input delay 40 ms + handler 320 ms + วาด 30 ms = 390 ms (ต้องปรับ) ถ้า yield หลังอัปเดต UI ให้ส่วนก่อน yield เหลือ 60 ms: 40 + 60 + 30 = 130 ms ผ่าน งานทั้งหมดยังทำครบ แค่ไม่ขวางเฟรมแรก
ตัวการอื่นที่เจอบ่อย: script ของบุคคลที่สาม (แชท, analytics, โฆษณา) ที่รันตอนผู้ใช้กำลังกด ทำให้ input delay สูง และ DOM ใหญ่มากที่ทำให้ presentation delay สูง
แก้ CLS: จองที่ไว้ก่อน
CLS วัดจากช่วงที่ของในหน้าเลื่อนติดกัน (ห่างกันไม่ถึง 1 วินาที ช่วงละไม่เกิน 5 วินาที) แล้วเอาช่วงที่แย่ที่สุด คะแนนแต่ละครั้ง = สัดส่วนพื้นที่จอที่ได้รับผลกระทบ × ระยะที่เลื่อนเทียบกับจอ เช่น ของกินพื้นที่ 75% ของจอ เลื่อนลง 25% ของความสูงจอ = 0.75 × 0.25 = 0.1875 ครั้งเดียวก็แย่แล้ว
- ใส่
widthและheightทุกรูปและวิดีโอ เบราว์เซอร์คำนวณสัดส่วนและจองพื้นที่ก่อนรูปมา - จองที่ให้โฆษณา embed และ banner ด้วย
min-heightหรือaspect-ratio - อย่าแทรกของเหนือเนื้อหาที่แสดงอยู่แล้ว เช่น แถบ cookie ที่ดันหน้าลง ให้ลอยทับแทน
- การเลื่อนที่เกิดภายใน 500 ms หลังผู้ใช้คลิก แตะ หรือกดแป้น ไม่นับ เช่น กดแล้วเมนูขยาย แต่ scroll, ลาก และ pinch-zoom ไม่นับเป็น input ของที่เลื่อนหลัง scroll ยังโดนนับ
ข้อผิดพลาดที่เจอบ่อย
- ดูแต่ Lighthouse บนเครื่อง dev เครื่องแรง เน็ตเร็ว ไม่มี extension ไม่เหมือนผู้ใช้จริงสักอย่าง
- ใส่
loading="lazy"ทุกรูปทั้งเว็บ รวมรูป hero ไปด้วย LCP แย่ลงทันที - ใส่
fetchpriority="high"ทุกรูป ไม่มีรูปไหนสำคัญกว่ากัน - ดูค่าเฉลี่ย แทน p75 อย่างในตัวอย่าง ค่าเฉลี่ย 2,435 ms ซ่อนคนห้าคนที่รอเกิน 3 วินาที
- แก้แล้วรอ CrUX วันเดียว ข้อมูลเป็นหน้าต่าง 28 วัน ต้องรอราวสี่สัปดาห์กว่าค่าจะสะท้อนการแก้เต็มที่ ระหว่างนั้นดูจากข้อมูล web-vitals ของเราเอง
- ใช้
scheduler.yield()โดยไม่มี fallback Safari จะ error
ลำดับที่ได้ผล: ข้อมูลจริงบอกว่าหน้าไหนแดง → attribution บอก element ไหน → แล็บใช้ลองแก้ → รอข้อมูลจริงยืนยัน
แหล่งอ้างอิง
- Web Vitals — web.dev
- Interaction to Next Paint becomes a Core Web Vital on March 12 — web.dev
- Optimize Largest Contentful Paint — web.dev และ Optimize long tasks
- Interaction to Next Paint (INP) — web.dev และ Cumulative Layout Shift (CLS)
- About PageSpeed Insights — Google for Developers
- CrUX methodology — Chrome for Developers
- web-vitals library — GitHub
คำถามที่พบบ่อย
Lighthouse ได้ 100 แล้ว ทำไม Search Console ยังบอกว่าไม่ผ่าน
Lighthouse คือข้อมูลแล็บ รันครั้งเดียวบนเครื่องจำลองโดยไม่มีใครคลิกอะไร ส่วน Search Console ใช้ข้อมูลจริงจาก Chrome ของผู้ใช้ย้อนหลัง 28 วัน ที่ percentile 75 คนใช้มือถือช้า เน็ตช้า หรือคลิกปุ่มหนัก ๆ จะไม่ปรากฏในแล็บ แต่ปรากฏในข้อมูลจริง
เว็บคนเข้าน้อย ไม่มีข้อมูล CrUX ทำยังไง
CrUX มีเกณฑ์ความนิยมขั้นต่ำที่ไม่เปิดเผยตัวเลข เว็บเล็กมักไม่มีข้อมูลระดับหน้า ให้เก็บเองด้วยไลบรารี web-vitals ส่งค่าเข้า endpoint ของเรา แล้วคำนวณ p75 เอง
FID ยังต้องสนใจไหม
ไม่ต้อง INP แทนที่ตั้งแต่มีนาคม 2024 Search Console เลิกแสดง FID วันเดียวกัน และไลบรารี web-vitals เวอร์ชันปัจจุบันไม่มีฟังก์ชัน onFID แล้ว
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ