Web Performance 2026 — Core Web Vitals, Speculation Rules API và View Transitions

Posted on: 4/17/2026 10:20:19 AM

Năm 2026, người dùng web kỳ vọng trải nghiệm ngang ngửa ứng dụng native — mọi tương tác phải phản hồi dưới 200ms, trang mới phải hiển thị gần như tức thì, và layout không được nhảy lung tung. Google đã chính thức thay thế FID bằng INP (Interaction to Next Paint), đồng thời trình duyệt hiện đại mang đến hai API đột phá: Speculation Rules API cho prerender/prefetch thông minh, và View Transitions API cho chuyển trang mượt mà. Bài viết này đi sâu vào kiến trúc tối ưu hiệu năng web toàn diện cho năm 2026.

< 200ms INP threshold — Good
24% Giảm bounce rate khi pass CWV
~0ms LCP khi dùng Speculation Rules prerender
93% Trình duyệt hỗ trợ View Transitions (2026)

1. Core Web Vitals 2026 — Ba trụ cột mới

Core Web Vitals là bộ ba chỉ số đo lường trải nghiệm người dùng thực tế, ảnh hưởng trực tiếp đến ranking trên Google Search. Năm 2026, bộ ba này gồm LCP, INP, và CLS — với INP là thay đổi lớn nhất.

LCP — Largest Contentful Paint

Thời gian render phần tử lớn nhất trong viewport (hình ảnh hero, heading chính, video poster). Phản ánh tốc độ tải "nội dung chính" mà người dùng quan tâm.

≤ 2.5s 2.5s — 4s > 4s

INP — Interaction to Next Paint

Đo latency từ lúc người dùng tương tác (click, tap, keypress) đến khi trình duyệt paint frame tiếp theo. Khác FID, INP đo toàn bộ tương tác trong vòng đời trang, không chỉ lần đầu.

≤ 200ms 200ms — 500ms > 500ms

CLS — Cumulative Layout Shift

Tổng điểm dịch chuyển layout bất ngờ trong suốt vòng đời trang. Ảnh hưởng bởi hình ảnh không có kích thước, font flash, nội dung inject động.

≤ 0.1 0.1 — 0.25 > 0.25
graph LR
    A[User Request] --> B[TTFB]
    B --> C[FCP]
    C --> D[LCP]
    D --> E[User Interaction]
    E --> F[INP]

    G[Layout Shift Events] --> H[CLS Score]

    style A fill:#e94560,stroke:#fff,color:#fff
    style D fill:#e94560,stroke:#fff,color:#fff
    style F fill:#e94560,stroke:#fff,color:#fff
    style H fill:#e94560,stroke:#fff,color:#fff
    style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style G fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    

Timeline các chỉ số Core Web Vitals trong vòng đời trang web

2. INP Deep Dive — Chỉ số khó nhất để tối ưu

INP có tỷ lệ pass thấp nhất trong ba chỉ số Core Web Vitals, bởi việc tối ưu đòi hỏi thay đổi kiến trúc JavaScript ở mức sâu — không đơn giản là thêm lazy loading hay nén ảnh.

2.1. Nguyên nhân INP cao

graph TD
    A[INP cao > 200ms] --> B[Long Tasks trên Main Thread]
    A --> C[Event Handler nặng]
    A --> D[Third-party Scripts]
    A --> E[Layout Thrashing]

    B --> B1[JavaScript bundle > 300KB]
    B --> B2[Hydration toàn trang]
    B --> B3[Parsing JSON lớn]

    C --> C1[Re-render toàn bộ component tree]
    C --> C2[Synchronous computation]
    C --> C3[DOM manipulation lớn]

    D --> D1[Analytics scripts]
    D --> D2[Chat widgets]
    D --> D3[Social media embeds]

    E --> E1[Read-write layout loop]
    E --> E2[Forced reflow]

    style A fill:#c62828,stroke:#fff,color:#fff
    style B fill:#e94560,stroke:#fff,color:#fff
    style C fill:#e94560,stroke:#fff,color:#fff
    style D fill:#e94560,stroke:#fff,color:#fff
    style E fill:#e94560,stroke:#fff,color:#fff
    style B1 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style B2 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style B3 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style C1 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style C2 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style C3 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style D1 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style D2 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style D3 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style E1 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style E2 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    

Nguyên nhân chính khiến INP vượt ngưỡng 200ms

2.2. Kỹ thuật tối ưu INP

a) Yield to Main Thread với scheduler.yield()

API mới scheduler.yield() cho phép "nhường" main thread giữa các tác vụ nặng, giúp trình duyệt xử lý input events và paint frames kịp thời:

scheduler-yield-example.js
async function processLargeDataset(items) {
  const results = [];

  for (let i = 0; i < items.length; i++) {
    results.push(transformItem(items[i]));

    // Yield mỗi 50 items để main thread xử lý user input
    if (i % 50 === 0 && i > 0) {
      await scheduler.yield();
    }
  }

  return results;
}

// Fallback cho trình duyệt chưa hỗ trợ
function yieldToMain() {
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }
  return new Promise(resolve => setTimeout(resolve, 0));
}

b) Web Workers cho tác vụ nặng

Chuyển các tính toán nặng sang Web Worker, giữ main thread chỉ xử lý UI:

search-worker.js
// Worker thread — chạy song song, không block main thread
self.onmessage = function(e) {
  const { data, query } = e.data;

  // Fuzzy search trên dataset lớn — nếu chạy trên main thread
  // sẽ block UI 200-500ms
  const results = data.filter(item =>
    item.title.toLowerCase().includes(query.toLowerCase()) ||
    item.content.toLowerCase().includes(query.toLowerCase())
  );

  // Ranking theo relevance
  results.sort((a, b) => scoreRelevance(b, query) - scoreRelevance(a, query));

  self.postMessage({ results: results.slice(0, 50) });
};
main.js — sử dụng Worker
const searchWorker = new Worker('/search-worker.js');

searchInput.addEventListener('input', debounce((e) => {
  searchWorker.postMessage({
    data: allProducts,
    query: e.target.value
  });
}, 150));

searchWorker.onmessage = (e) => {
  renderSearchResults(e.data.results);
};

Mẹo thực tế

Đặt touch target tối thiểu 48x48 pixels và cung cấp visual feedback ngay lập tức (CSS :active state) cho mọi nút bấm. Người dùng cảm nhận tương tác nhanh hơn khi có phản hồi thị giác, ngay cả khi processing chưa xong.

c) Tối ưu INP trên Vue.js

Vue.js có một số kỹ thuật đặc thù giúp giảm INP đáng kể:

OptimizedList.vue — shallowRef cho dataset lớn
<script setup>
import { shallowRef, computed, triggerRef } from 'vue'

// shallowRef: chỉ track reference change, không deep-watch
// Với 10,000+ items, tiết kiệm ~60% reactivity overhead
const products = shallowRef([])

// Computed với lazy evaluation — chỉ tính khi template cần
const filteredProducts = computed(() => {
  return products.value.filter(p => p.active)
})

async function loadProducts() {
  const data = await fetch('/api/products').then(r => r.json())
  products.value = data  // trigger re-render
}

function updateProduct(id, changes) {
  const idx = products.value.findIndex(p => p.id === id)
  if (idx !== -1) {
    products.value[idx] = { ...products.value[idx], ...changes }
    triggerRef(products)  // manual trigger vì shallowRef
  }
}
</script>

<template>
  <!-- v-once cho static content, giảm re-render cost -->
  <header v-once>
    <h1>Product Catalog</h1>
    <p>Browse our collection</p>
  </header>

  <!-- Virtual scroll cho list dài -->
  <VirtualList :items="filteredProducts" :item-height="80">
    <template #default="{ item }">
      <ProductCard :product="item" @update="updateProduct" />
    </template>
  </VirtualList>
</template>

Bundle size budget cho Vue apps (2026)

Marketing pages: ≤ 200KB JS | Transactional pages: ≤ 300KB JS | Dashboards: ≤ 350KB JS. Vượt ngưỡng này, INP gần như chắc chắn > 200ms trên thiết bị mid-range.

3. Speculation Rules API — Prerender thông minh

Speculation Rules API cho phép khai báo trước những trang mà người dùng có khả năng điều hướng đến. Trình duyệt sẽ prefetch (tải trước tài nguyên) hoặc prerender (render sẵn toàn bộ trang) ở background, giúp navigation gần như tức thì.

sequenceDiagram
    participant U as User
    participant B as Browser
    participant S as Server

    Note over B: Đọc Speculation Rules
    B->>S: Prerender /pricing (background)
    S-->>B: HTML + CSS + JS (cached)
    Note over B: Render sẵn /pricing trong hidden tab

    U->>B: Click link /pricing
    Note over B: Swap prerendered page ~0ms
    B-->>U: Trang /pricing hiển thị tức thì

    Note over U,B: LCP ≈ 0ms vì đã prerender sẵn
    

Flow prerender với Speculation Rules API — trang đích đã sẵn sàng trước khi user click

3.1. Cú pháp khai báo

Speculation Rules sử dụng JSON khai báo trực tiếp trong HTML hoặc qua HTTP header:

Cách 1: Inline trong HTML
<script type="speculationrules">
{
  "prerender": [
    {
      "source": "document",
      "where": {
        "selector_matches": "nav a, a.cta-button"
      },
      "eagerness": "moderate"
    }
  ],
  "prefetch": [
    {
      "source": "document",
      "where": {
        "selector_matches": ".blog-list a"
      },
      "eagerness": "conservative"
    }
  ]
}
</script>
Cách 2: HTTP Header trỏ đến file JSON
Speculation-Rules: "/speculation-rules.json"

// File speculation-rules.json (MIME: application/speculationrules+json)
{
  "prerender": [
    {
      "source": "list",
      "urls": ["/pricing", "/checkout", "/dashboard"]
    }
  ]
}

3.2. Eagerness levels

Eagerness Trigger Use case Bandwidth cost
immediate Ngay khi rule được parse Trang tiếp theo gần như chắc chắn (checkout flow) Cao
eager Tương tự immediate, ít ưu tiên hơn Top navigation links Cao
moderate Hover trên link ~200ms Navigation chính, CTA buttons Trung bình
conservative Pointerdown hoặc touchstart Blog list, search results Thấp

Khi nào dùng prefetch vs prerender?

Prefetch: tải trước HTML và critical resources — tiết kiệm bandwidth, phù hợp cho danh sách nhiều link (blog list, search results).
Prerender: render toàn bộ trang sẵn — tốn nhiều tài nguyên hơn nhưng navigation gần tức thì, phù hợp cho CTA flow (pricing → checkout) hoặc navigation chính.

3.3. Tích hợp Speculation Rules vào Vue Router

useSpeculationRules.ts — composable cho Vue 3
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'

export function useSpeculationRules() {
  const router = useRouter()
  let scriptEl: HTMLScriptElement | null = null

  function updateRules() {
    // Xóa rules cũ
    scriptEl?.remove()

    // Lấy tất cả route paths có thể navigate
    const routes = router.getRoutes()
      .filter(r => !r.meta?.noPrerender)
      .map(r => r.path)
      .slice(0, 10)  // Giới hạn 10 trang để tiết kiệm tài nguyên

    const rules = {
      prerender: [{
        source: "list",
        urls: routes.filter(r => r === '/pricing' || r === '/checkout'),
        eagerness: "moderate"
      }],
      prefetch: [{
        source: "list",
        urls: routes.filter(r => r !== '/pricing' && r !== '/checkout'),
        eagerness: "conservative"
      }]
    }

    scriptEl = document.createElement('script')
    scriptEl.type = 'speculationrules'
    scriptEl.textContent = JSON.stringify(rules)
    document.head.appendChild(scriptEl)
  }

  onMounted(updateRules)
  onUnmounted(() => scriptEl?.remove())

  return { updateRules }
}

4. View Transitions API — Chuyển trang mượt như Native App

View Transitions API cho phép trình duyệt thực hiện animated transitions giữa hai trạng thái DOM — cả trong SPA và cross-document navigation (MPA). Kết hợp với Speculation Rules, ta có navigation vừa nhanh vừa mượt.

graph LR
    subgraph "Trước View Transitions"
        A1[Page A] -->|"Hard cut - flash trắng"| B1[Page B]
    end

    subgraph "Với View Transitions"
        A2[Page A] -->|"Capture snapshot"| T[Transition Layer]
        T -->|"Crossfade + morph"| B2[Page B]
    end

    style A1 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style B1 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style A2 fill:#e94560,stroke:#fff,color:#fff
    style T fill:#2c3e50,stroke:#fff,color:#fff
    style B2 fill:#e94560,stroke:#fff,color:#fff
    

View Transitions tạo layer chuyển tiếp giữa hai trạng thái, loại bỏ hiệu ứng "flash trắng"

4.1. Cross-document View Transitions (MPA)

Cho các trang multi-page truyền thống (bao gồm .NET Core MVC), chỉ cần thêm meta tag:

_Layout.cshtml — .NET Core MVC
<!-- Opt-in View Transitions cho cross-document navigation -->
<meta name="view-transition" content="same-origin" />

<style>
/* Transition mặc định: crossfade */
::view-transition-old(root) {
  animation: fade-out 0.2s ease-in;
}
::view-transition-new(root) {
  animation: fade-in 0.3s ease-out;
}

/* Named transition cho hero image — morph giữa list và detail */
.blog-card img {
  view-transition-name: hero-image;
}
.blog-detail .hero {
  view-transition-name: hero-image;
}

::view-transition-group(hero-image) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in { from { opacity: 0; } }
</style>

4.2. SPA View Transitions với Vue Router

router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [/* ... */]
})

// Wrap mỗi navigation trong View Transition
router.beforeResolve(async (to, from) => {
  // Chỉ apply transition khi API được hỗ trợ
  if (!document.startViewTransition) return

  // Đặt class hint để CSS biết direction
  const direction = to.meta?.index > from.meta?.index
    ? 'forward' : 'back'
  document.documentElement.dataset.transition = direction

  await new Promise(resolve => {
    const transition = document.startViewTransition(() => {
      resolve(undefined)
    })
  })
})

export default router
transitions.css
/* Slide transition dựa trên direction */
[data-transition="forward"]::view-transition-old(root) {
  animation: slide-out-left 0.3s ease-in;
}
[data-transition="forward"]::view-transition-new(root) {
  animation: slide-in-right 0.3s ease-out;
}
[data-transition="back"]::view-transition-old(root) {
  animation: slide-out-right 0.3s ease-in;
}
[data-transition="back"]::view-transition-new(root) {
  animation: slide-in-left 0.3s ease-out;
}

@keyframes slide-out-left { to { transform: translateX(-100%); opacity: 0; } }
@keyframes slide-in-right { from { transform: translateX(100%); opacity: 0; } }
@keyframes slide-out-right { to { transform: translateX(100%); opacity: 0; } }
@keyframes slide-in-left { from { transform: translateX(-100%); opacity: 0; } }

5. Combo Tối Thượng: Speculation Rules + View Transitions

Khi kết hợp cả hai API, trình duyệt prerender trang đích sẵn rồi animate transition giữa hai trang đã render. Kết quả: LCP ≈ 0ms và transition mượt mà — trải nghiệm không thể phân biệt với native app.

sequenceDiagram
    participant U as User
    participant B as Browser (Main)
    participant P as Prerender Tab
    participant S as Server

    Note over B: Parse Speculation Rules
    B->>S: GET /destination (background)
    S-->>P: Full page HTML + assets
    Note over P: Render hoàn chỉnh (hidden)

    U->>B: Hover link /destination
    Note over B: moderate eagerness → đã prerender xong

    U->>B: Click link /destination
    Note over B,P: startViewTransition()
    B->>B: Capture old snapshot
    B->>P: Swap to prerendered page
    B->>B: Animate old → new
    B-->>U: Smooth transition ~300ms
    Note over U: LCP = 0ms, visual transition mượt
    

Speculation Rules prerender + View Transitions = instant & smooth navigation

Cấu hình đầy đủ cho landing page
<!DOCTYPE html>
<html>
<head>
  <!-- 1. Opt-in View Transitions -->
  <meta name="view-transition" content="same-origin" />

  <!-- 2. Speculation Rules -->
  <script type="speculationrules">
  {
    "prerender": [
      {
        "source": "document",
        "where": { "selector_matches": "nav a, .cta-primary" },
        "eagerness": "moderate"
      }
    ],
    "prefetch": [
      {
        "source": "document",
        "where": { "selector_matches": ".blog-list a, footer a" },
        "eagerness": "conservative"
      }
    ]
  }
  </script>

  <style>
    /* 3. View Transition animations */
    ::view-transition-old(root) {
      animation: fade-and-scale-out 0.25s ease-in forwards;
    }
    ::view-transition-new(root) {
      animation: fade-and-scale-in 0.35s ease-out forwards;
    }
    @keyframes fade-and-scale-out {
      to { opacity: 0; transform: scale(0.95); }
    }
    @keyframes fade-and-scale-in {
      from { opacity: 0; transform: scale(1.05); }
    }
  </style>
</head>
<body>
  <!-- Content -->
</body>
</html>

6. LCP Optimization Checklist

Ngoài Speculation Rules (giải quyết LCP triệt để cho navigation giữa các trang), vẫn cần tối ưu LCP cho lần tải đầu tiên:

Kỹ thuật Impact Implementation
Preload LCP resource -200ms đến -1s <link rel="preload" as="image" href="hero.webp" fetchpriority="high">
fetchpriority="high" -100ms đến -500ms Thêm vào LCP image/element để browser ưu tiên tải
AVIF/WebP format -30% đến -50% size <picture> với fallback: AVIF → WebP → JPEG
CDN + Edge caching -50ms đến -200ms TTFB Cache HTML tại edge, stale-while-revalidate cho assets
Server-side rendering -500ms đến -2s .NET Core: MapRazorPages() hoặc Vue SSR với Nuxt
Inline critical CSS -100ms đến -300ms Embed above-the-fold CSS trong <head>, defer phần còn lại

7. CLS — Giữ layout ổn định

CLS đo sự dịch chuyển layout bất ngờ. Năm 2026, nguyên nhân phổ biến nhất là web fonts, hình ảnh không kích thước, và quảng cáo/embed inject muộn.

7.1. Kỹ thuật triệt tiêu CLS

anti-cls.css
/* 1. Luôn set aspect-ratio hoặc width/height cho media */
img, video {
  max-width: 100%;
  height: auto;
  aspect-ratio: attr(width) / attr(height);
}

/* 2. Font display swap + size-adjust để giảm layout shift */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
  /* size-adjust khớp fallback font → giảm shift khi swap */
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

/* 3. Reserve space cho dynamic content */
.ad-slot {
  min-height: 250px;  /* Giữ chỗ trước khi ad load */
  contain: layout;     /* CSS Containment ngăn ảnh hưởng layout */
}

/* 4. content-visibility cho offscreen sections */
.below-fold-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;  /* Ước lượng chiều cao */
}

CSS Containment — vũ khí ẩn cho CLS

contain: layout nói với trình duyệt rằng nội dung bên trong element không ảnh hưởng layout bên ngoài. Kết hợp content-visibility: auto giúp browser skip rendering cho off-screen sections, giảm cả CLS lẫn render time.

8. Islands Architecture — Selective Hydration

Thay vì hydrate toàn bộ trang (ship cả megabyte JS cho mỗi trang), Islands Architecture chỉ hydrate các "đảo" tương tác. Phần static HTML còn lại không cần JavaScript — giảm đáng kể bundle size và INP.

graph TD
    subgraph "Traditional SPA"
        T1[Full Page JS Bundle 450KB] --> T2[Hydrate toàn bộ DOM]
        T2 --> T3[INP bị block 300-800ms]
    end

    subgraph "Islands Architecture"
        I1[Static HTML - 0KB JS] --> I2[Island: Search 25KB]
        I1 --> I3[Island: Cart 18KB]
        I1 --> I4[Island: Comments 30KB]
        I2 --> I5[Chỉ hydrate khi visible]
        I3 --> I5
        I4 --> I5
        I5 --> I6[INP < 100ms]
    end

    style T1 fill:#c62828,stroke:#fff,color:#fff
    style T3 fill:#c62828,stroke:#fff,color:#fff
    style I1 fill:#4CAF50,stroke:#fff,color:#fff
    style I6 fill:#4CAF50,stroke:#fff,color:#fff
    style I2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style I3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style I4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style I5 fill:#e94560,stroke:#fff,color:#fff
    style T2 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    

So sánh JS budget: SPA truyền thống vs Islands Architecture

Trong ecosystem Vue, Nuxt 4 hỗ trợ Islands Architecture native qua <NuxtIsland> component và server components. Với .NET Core, pattern tương tự có thể đạt được bằng Blazor SSR kết hợp enhanced navigation.

9. Đo lường và Monitoring

Tối ưu mà không đo lường thì chỉ là đoán mò. Dưới đây là pipeline giám sát Core Web Vitals đề xuất:

graph LR
    A[web-vitals.js Library] -->|Real User Data| B[Analytics Endpoint]
    B --> C[Dashboard / Alerting]

    D[Lighthouse CI] -->|Synthetic Tests| C
    E[CrUX API] -->|28-day Field Data| C

    C --> F{Pass CWV?}
    F -->|Yes| G[Monitor & Maintain]
    F -->|No| H[Debug với DevTools Performance Panel]
    H --> I[Fix & Deploy]
    I --> A

    style A fill:#e94560,stroke:#fff,color:#fff
    style C fill:#2c3e50,stroke:#fff,color:#fff
    style F fill:#e94560,stroke:#fff,color:#fff
    style G fill:#4CAF50,stroke:#fff,color:#fff
    style H fill:#ff9800,stroke:#fff,color:#fff
    

Pipeline đo lường CWV: kết hợp RUM, synthetic testing, và CrUX data

9.1. Tích hợp web-vitals vào production

vitals-reporter.js
import { onINP, onLCP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,  // "good" | "needs-improvement" | "poor"
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
    url: location.href,
    // Attribution data giúp debug root cause
    ...(metric.attribution && {
      element: metric.attribution.element,
      largestShiftTarget: metric.attribution.largestShiftTarget,
      interactionTarget: metric.attribution.interactionTarget,
    })
  });

  // sendBeacon đảm bảo data được gửi ngay cả khi user rời trang
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

// Đo tất cả Core Web Vitals với attribution
onLCP(sendToAnalytics, { reportAllChanges: true });
onINP(sendToAnalytics, { reportAllChanges: true });
onCLS(sendToAnalytics, { reportAllChanges: true });

9.2. Endpoint thu thập trên .NET Core

VitalsController.cs — Minimal API .NET
app.MapPost("/api/vitals", async (HttpContext ctx) =>
{
    var metric = await ctx.Request.ReadFromJsonAsync<WebVitalMetric>();

    // Log structured data cho monitoring
    logger.LogInformation(
        "CWV {Name}: {Value}ms rating={Rating} url={Url} element={Element}",
        metric.Name, metric.Value, metric.Rating,
        metric.Url, metric.Element);

    // Có thể push vào time-series DB để dashboard
    await metricsService.RecordAsync(metric);

    return Results.Ok();
});

record WebVitalMetric(
    string Name, double Value, string Rating,
    double Delta, string Url, string? Element);

10. Performance Budget và CI/CD Gate

Thiết lập performance budget trong CI pipeline đảm bảo không ai vô tình ship code làm tụt Core Web Vitals:

lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "interactive": ["error", { "maxNumericValue": 3800 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["error", { "maxNumericValue": 300 }]
      }
    },
    "collect": {
      "numberOfRuns": 3,
      "url": ["http://localhost:3000/", "http://localhost:3000/blog"]
    }
  }
}

Performance budget thực tế cho 2026

Lighthouse score ≥ 90 là baseline, nhưng đừng chỉ dựa vào synthetic tests. Kết hợp CrUX data (dữ liệu thực tế từ Chrome users) qua Chrome UX Report API để thấy performance trên thiết bị và mạng thực tế của người dùng.

11. Tổng kết chiến lược

Chỉ số Chiến lược chính API / Tool Expected Impact
LCP Prerender + preload + SSR Speculation Rules, fetchpriority, .NET SSR ≈ 0ms (prerendered) / < 2s (first load)
INP Yield main thread + selective hydration scheduler.yield(), Web Workers, Islands < 150ms trên mid-range devices
CLS Reserve space + font stability aspect-ratio, size-adjust, content-visibility < 0.05
Navigation UX Animated transitions View Transitions API + Speculation Rules Native-app feel, ~0ms perceived latency
Monitoring RUM + synthetic CI gates web-vitals.js, Lighthouse CI, CrUX Catch regressions before production

Web Performance 2026 không còn chỉ là nén ảnh và minify JS. Với Speculation Rules API, View Transitions API, và các kỹ thuật tối ưu INP hiện đại, ta có thể mang đến trải nghiệm web ngang ngửa native app — trang tải tức thì, tương tác mượt mà, và chuyển cảnh đẹp mắt. Hãy bắt đầu đo lường, thiết lập performance budget, và tích hợp các API mới vào pipeline phát triển của bạn ngay hôm nay.

Nguồn tham khảo