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

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

In 2026, web users expect an experience on par with native apps — every interaction must respond in under 200 ms, new pages must appear almost instantly, and layout can't jump around. Google officially replaced FID with INP (Interaction to Next Paint), and modern browsers ship two breakthrough APIs: Speculation Rules API for smart prerender/prefetch, and View Transitions API for smooth page transitions. This article dives into a comprehensive web-performance architecture for 2026.

< 200ms INP threshold — Good
24% Bounce rate drop when CWV pass
~0ms LCP with Speculation Rules prerender
93% Browsers supporting View Transitions (2026)

1. Core Web Vitals 2026 — Three new pillars

Core Web Vitals is the trio that measures real-user experience and directly influences Google Search ranking. In 2026 the trio is LCP, INP, and CLS — with INP being the biggest change.

LCP — Largest Contentful Paint

Time to render the largest element in the viewport (hero image, main heading, video poster). Reflects how quickly the "main content" users care about appears.

≤ 2.5s 2.5s — 4s > 4s

INP — Interaction to Next Paint

Latency from a user interaction (click, tap, keypress) to the next paint. Unlike FID, INP measures every interaction during the page lifetime, not only the first.

≤ 200ms 200ms — 500ms > 500ms

CLS — Cumulative Layout Shift

Total score of unexpected layout shifts across the page's lifetime. Affected by images without dimensions, font flashes, and late-injected dynamic content.

≤ 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
    

Core Web Vitals timeline across a page's lifecycle

2. INP deep dive — The hardest metric to optimize

INP has the lowest pass rate among the three Core Web Vitals because optimizing it requires deep architectural changes to JavaScript — not just adding lazy loading or compressing images.

2.1. Why INP is high

graph TD
    A[INP > 200ms] --> B[Long tasks on the main thread]
    A --> C[Heavy event handlers]
    A --> D[Third-party scripts]
    A --> E[Layout thrashing]

    B --> B1[JavaScript bundle > 300KB]
    B --> B2[Full-page hydration]
    B --> B3[Parsing large JSON]

    C --> C1[Re-rendering the whole component tree]
    C --> C2[Synchronous computation]
    C --> C3[Large DOM manipulation]

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

    E --> E1[Read-write layout loops]
    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
    

Primary causes of INP exceeding 200 ms

2.2. INP optimization techniques

a) Yield to the main thread with scheduler.yield()

The new scheduler.yield() API lets you "yield" the main thread between heavy tasks so the browser can handle input events and paint frames in time:

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

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

    // Yield every 50 items to let the main thread handle user input
    if (i % 50 === 0 && i > 0) {
      await scheduler.yield();
    }
  }

  return results;
}

// Fallback for browsers without support
function yieldToMain() {
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }
  return new Promise(resolve => setTimeout(resolve, 0));
}

b) Web Workers for heavy tasks

Move heavy computation to a Web Worker, keeping the main thread for UI only:

search-worker.js
// Worker thread — runs in parallel, doesn't block the main thread
self.onmessage = function(e) {
  const { data, query } = e.data;

  // Fuzzy search over a large dataset — if run on the main thread
  // it would block the UI for 200-500ms
  const results = data.filter(item =>
    item.title.toLowerCase().includes(query.toLowerCase()) ||
    item.content.toLowerCase().includes(query.toLowerCase())
  );

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

  self.postMessage({ results: results.slice(0, 50) });
};
main.js — using the 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);
};

Practical tip

Keep touch targets at least 48×48 pixels and provide instant visual feedback (CSS :active state) on every button. Users perceive interactions as faster when they get visual feedback, even if processing isn't done.

c) INP optimization in Vue.js

Vue.js has a few specific techniques that meaningfully reduce INP:

OptimizedList.vue — shallowRef for large datasets
<script setup>
import { shallowRef, computed, triggerRef } from 'vue'

// shallowRef: tracks reference changes only, no deep-watch
// With 10,000+ items it saves ~60% reactivity overhead
const products = shallowRef([])

// Computed with lazy evaluation — only runs when the template needs it
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  // triggers 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 because of shallowRef
  }
}
</script>

<template>
  <!-- v-once for static content, reducing re-render cost -->
  <header v-once>
    <h1>Product Catalog</h1>
    <p>Browse our collection</p>
  </header>

  <!-- Virtual scroll for long lists -->
  <VirtualList :items="filteredProducts" :item-height="80">
    <template #default="{ item }">
      <ProductCard :product="item" @update="updateProduct" />
    </template>
  </VirtualList>
</template>

Bundle size budget for Vue apps (2026)

Marketing pages: ≤ 200 KB JS | Transactional pages: ≤ 300 KB JS | Dashboards: ≤ 350 KB JS. Beyond that, INP almost always crosses 200 ms on mid-range devices.

3. Speculation Rules API — Smart prerender

The Speculation Rules API lets you declare pages the user is likely to navigate to. The browser then prefetches (loads resources ahead) or prerenders (fully renders the page) in the background — making navigation feel instant.

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

    Note over B: Read Speculation Rules
    B->>S: Prerender /pricing (background)
    S-->>B: HTML + CSS + JS (cached)
    Note over B: Render /pricing in a hidden tab

    U->>B: Click link /pricing
    Note over B: Swap prerendered page ~0ms
    B-->>U: /pricing shows up instantly

    Note over U,B: LCP ≈ 0ms because it's already prerendered
    

Prerender flow with the Speculation Rules API — the destination is ready before the click

3.1. Declaration syntax

Speculation Rules use JSON declared directly in HTML or via an HTTP header:

Option 1: inline in 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>
Option 2: HTTP header pointing to a JSON file
Speculation-Rules: "/speculation-rules.json"

// 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 As soon as the rule is parsed Next page almost certainly follows (checkout flow) High
eager Similar to immediate but lower priority Top navigation links High
moderate Hover on a link for ~200 ms Primary navigation, CTA buttons Medium
conservative Pointerdown or touchstart Blog lists, search results Low

When to use prefetch vs prerender?

Prefetch: preloads HTML and critical resources — saves bandwidth, ideal for pages with many links (blog lists, search results).
Prerender: fully renders the page — more resource-intensive but navigation is nearly instant, ideal for CTA flows (pricing → checkout) or main navigation.

3.3. Integrating Speculation Rules into Vue Router

useSpeculationRules.ts — a composable for 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() {
    // Remove old rules
    scriptEl?.remove()

    // Collect all navigable route paths
    const routes = router.getRoutes()
      .filter(r => !r.meta?.noPrerender)
      .map(r => r.path)
      .slice(0, 10)  // Cap at 10 pages to save resources

    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 — Native-app smoothness

The View Transitions API lets the browser animate between two DOM states — in SPAs and in cross-document (MPA) navigation. Combined with Speculation Rules, navigation is both fast and smooth.

graph LR
    subgraph "Before View Transitions"
        A1[Page A] -->|"Hard cut — white flash"| B1[Page B]
    end

    subgraph "With 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 create a transition layer between the two states, eliminating the "white flash"

4.1. Cross-document View Transitions (MPA)

For traditional multi-page sites (including .NET Core MVC), just add a meta tag:

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

<style>
/* Default transition: 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 for the hero image — morph between list and 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 with Vue Router

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

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

// Wrap every navigation in a View Transition
router.beforeResolve(async (to, from) => {
  // Only apply when the API is supported
  if (!document.startViewTransition) return

  // Set a hint class so CSS knows the 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 based on 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. The ultimate combo: Speculation Rules + View Transitions

Combining both APIs, the browser prerenders the destination then animates a transition between the two rendered pages. The result: LCP ≈ 0 ms and smooth transitions — an experience indistinguishable from native apps.

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: Fully rendered (hidden)

    U->>B: Hover /destination link
    Note over B: moderate eagerness → already prerendered

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

Speculation Rules prerender + View Transitions = instant & smooth navigation

Full setup for a landing page
<!DOCTYPE html>
<html>
<head>
  <!-- 1. Opt-in to 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

Besides Speculation Rules (which largely solves LCP for cross-page navigation), you still need to optimize LCP for the initial load:

Technique Impact Implementation
Preload the LCP resource -200 ms to -1 s <link rel="preload" as="image" href="hero.webp" fetchpriority="high">
fetchpriority="high" -100 ms to -500 ms Add to the LCP image/element so the browser prioritizes it
AVIF/WebP format -30% to -50% size <picture> with fallback: AVIF → WebP → JPEG
CDN + Edge caching -50 ms to -200 ms TTFB Cache HTML at the edge, stale-while-revalidate for assets
Server-side rendering -500 ms to -2 s .NET Core: MapRazorPages(); Vue SSR via Nuxt
Inline critical CSS -100 ms to -300 ms Embed above-the-fold CSS inside <head>, defer the rest

7. CLS — Keeping layout stable

CLS measures unexpected layout shifts. In 2026, the most common causes are web fonts, images without dimensions, and late-injected ads/embeds.

7.1. Techniques to eliminate CLS

anti-cls.css
/* 1. Always set aspect-ratio or width/height for media */
img, video {
  max-width: 100%;
  height: auto;
  aspect-ratio: attr(width) / attr(height);
}

/* 2. font-display: swap + size-adjust to reduce layout shift */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
  /* size-adjust matches the fallback font → reduces shift on swap */
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

/* 3. Reserve space for dynamic content */
.ad-slot {
  min-height: 250px;  /* Hold the space before the ad loads */
  contain: layout;     /* CSS Containment prevents influence on surrounding layout */
}

/* 4. content-visibility for offscreen sections */
.below-fold-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;  /* Estimated height */
}

CSS Containment — the hidden weapon for CLS

contain: layout tells the browser that the element's internals don't affect the outer layout. Combined with content-visibility: auto, the browser skips rendering off-screen sections, reducing both CLS and render time.

8. Islands Architecture — Selective hydration

Instead of hydrating the entire page (shipping megabytes of JS per page), Islands Architecture hydrates only interactive "islands". The static HTML requires no JavaScript — significantly reducing bundle size and INP.

graph TD
    subgraph "Traditional SPA"
        T1[Full Page JS Bundle 450KB] --> T2[Hydrate the entire DOM]
        T2 --> T3[INP blocked 300-800 ms]
    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[Hydrate only when visible]
        I3 --> I5
        I4 --> I5
        I5 --> I6[INP < 100 ms]
    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
    

JS budget comparison: traditional SPA vs Islands Architecture

In the Vue ecosystem, Nuxt 4 natively supports Islands Architecture via the <NuxtIsland> component and server components. On .NET Core, a similar pattern is achievable with Blazor SSR + enhanced navigation.

9. Measurement and monitoring

Optimizing without measuring is guesswork. Here's a recommended Core Web Vitals monitoring pipeline:

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 with the 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
    

A CWV measurement pipeline combining RUM, synthetic testing, and CrUX data

9.1. Integrating web-vitals into 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 helps debug the root cause
    ...(metric.attribution && {
      element: metric.attribution.element,
      largestShiftTarget: metric.attribution.largestShiftTarget,
      interactionTarget: metric.attribution.interactionTarget,
    })
  });

  // sendBeacon guarantees delivery even when the user leaves the page
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

// Measure every Core Web Vital with attribution
onLCP(sendToAnalytics, { reportAllChanges: true });
onINP(sendToAnalytics, { reportAllChanges: true });
onCLS(sendToAnalytics, { reportAllChanges: true });

9.2. Collection endpoint on .NET Core

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

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

    // Optionally push to a time-series DB for dashboards
    await metricsService.RecordAsync(metric);

    return Results.Ok();
});

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

10. Performance budget and CI/CD gates

A performance budget in CI ensures no one accidentally ships code that regresses 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"]
    }
  }
}

A realistic 2026 performance budget

Lighthouse score ≥ 90 is the baseline, but don't rely only on synthetic tests. Combine CrUX data (real data from Chrome users) via the Chrome UX Report API to understand performance on real user devices and networks.

11. Strategy summary

Metric Primary strategy API / Tool Expected impact
LCP Prerender + preload + SSR Speculation Rules, fetchpriority, .NET SSR ≈ 0 ms (prerendered) / < 2 s (first load)
INP Yield main thread + selective hydration scheduler.yield(), Web Workers, Islands < 150 ms on 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, ~0 ms perceived latency
Monitoring RUM + synthetic CI gates web-vitals.js, Lighthouse CI, CrUX Catch regressions before production

Web performance in 2026 is no longer just about compressing images and minifying JS. With the Speculation Rules API, the View Transitions API, and modern INP optimization techniques, you can deliver an experience on par with native apps — instant page loads, smooth interactions, and beautiful transitions. Start measuring, set a performance budget, and integrate these new APIs into your development pipeline today.

References