agentsclimarketplace

Next on vercel perf

Skill SkillMedev/vercel-platform/skills/next-on-vercel-perf

A curated, opinionated path for shipping a Next.js app on Vercel — deploys, env, AI Gateway, rendering & cachi

Install
npx -y skills add SkillMedev/vercel-platform --skill next-on-vercel-perf

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Diagnose and fix Core Web Vitals and page performance of a Next.js app on Vercel - measurement-first. Use when an LCP/INP/CLS score is bad, a page "feels slow", Lighthouse/PageSpeed/Speed Insights is red, the JS bundle is too big, images or fonts are janky, or you want Vercel-specific caching/edge wins. Triggers: "improve Core Web Vitals", "fix LCP", "reduce INP", "stop layout shift", "my Next.js page is slow on Vercel", "shrink my bundle", "next/image optimization", "font is causing CLS", "cache this page". Do NOT use to set up the deploy pipeline or rollbacks - use vercel-deploy-pipeline; for ISR/cache-component semantics and edge runtime choice - use vercel-edge-and-isr; for env/secrets - use vercel-env-management; for model/provider routing - use vercel-ai-gateway; for WAF/bot defense - use vercel-firewall-and-botid.

SKILL.md

16.5 KB, as published. Nobody here has run it

Next on Vercel Performance

You make a Next.js app on Vercel measurably faster - by measuring first, fixing the one metric that is actually red, and verifying the number moved. Performance work without a number in front of you is guesswork; this skill refuses to guess.

Trigger eagerly on any signal that a page is slow or a vital is failing: "fix my LCP", "reduce INP", "stop the layout shift", "Lighthouse is red", "Speed Insights is bad", "my bundle is huge", "the page feels janky on mobile", "make this page faster on Vercel". Each Core Web Vital has a different root cause and a different fix - never apply an INP fix to an LCP problem. This skill is one stop in the ship-a-Next.js-app-on-Vercel workflow: it assumes the app already deploys (vercel-deploy-pipeline) and that ISR / runtime choices live in vercel-edge-and-isr; here we tune what those produce.

Platform facts this skill assumes (current as of 2026): Node.js 24 is the default runtime and Fluid Compute is the default compute model (full Node.js, instance reuse cuts cold starts) - Edge Functions are no longer recommended, so do NOT reach for the edge runtime as a performance lever by default. Reserve runtime decisions for vercel-edge-and-isr.

Step 1 - Get a real measurement before touching code

Do not open a component until you have a number. There are two measurement worlds and you need both:

  • Field data (real users, what Google ranks on): turn on Vercel Speed Insights (@vercel/speed-insights) to collect LCP / INP / CLS / TTFB / FCP from production traffic. In CI or a script, pull the field numbers with the CLI:
# requires Speed Insights enabled + Observability Plus on the project
vercel metrics
  • Lab data (reproducible, one page): run Lighthouse against a production deployment (never next dev - dev is unoptimized and lies about every number):
npm i -g lighthouse
lighthouse https://your-app.vercel.app/the-slow-page \
  --only-categories=performance --form-factor=mobile --output=json \
  --output-path=./lh.json --quiet

Write down the failing metric and its value (e.g. "mobile LCP 4.8s, target ≤2.5s"). That number is your acceptance test. Field and lab disagree often - trust field data for what to fix, use lab data to iterate quickly.

The 2026 thresholds (mobile, "good"):

MetricGoodNeeds workPoorWhat it measures
LCP≤ 2.5s≤ 4.0s> 4.0sTime to render the largest element (loading)
INP≤ 200ms≤ 500ms> 500msSlowest interaction→paint (responsiveness)
CLS≤ 0.1≤ 0.25> 0.25Unexpected layout movement (visual stability)

Step 2 - Route the failing metric to its root cause

Each vital has one dominant cause family. Diagnose, do not shotgun.

  • LCP slow → the largest element (usually a hero image or a heading blocked by a font/by data). Causes: unoptimized hero image, slow TTFB (uncached dynamic render), render-blocking JS/CSS, font swap delaying text. Go to Step 3 (images), Step 5 (fonts), Step 6 (caching/TTFB).
  • INP high → main-thread blocked during interaction by too much JavaScript: oversized client bundles, heavy hydration, expensive event handlers. Go to Step 4 (bundle) - this is almost always a "ship less JS" problem.
  • CLS high → elements without reserved space: images/embeds/ads without dimensions, fonts swapping metrics, content injected above the fold. Go to Step 3 (image width/height) and Step 5 (next/font + size-adjust).

Run vitals_triage.js (below) with your numbers to get the ordered fix list.

Step 3 - Images: the #1 LCP and CLS lever

Most LCP and CLS wins are one image. Use next/image, which on Vercel runs the native Image Optimization API (on-demand resize, AVIF/WebP, lazy-load, cached at the edge):

import Image from 'next/image'

// Above-the-fold hero (the LCP element): priority loads it eagerly, no lazy delay.
<Image
  src="/hero.jpg"
  alt="Product hero"
  width={1280}
  height={720}     // width+height reserve space → zero layout shift (CLS)
  priority         // ONLY on the LCP image - preloads, skips lazy-loading
  sizes="(max-width: 768px) 100vw, 1280px"  // stops over-large downloads on mobile
  placeholder="blur"
/>

Rules that actually move the number:

  • priority on the LCP image only. It preloads. Putting it on everything re-creates the problem by preloading offscreen images.
  • Always pass width and height (or fill + a sized parent). This is the single biggest CLS fix - the browser reserves the box before the bytes arrive.
  • Set sizes so mobile fetches a small variant, not the 1280px desktop file.
  • Prefer AVIF then WebP. Configure formats in vercel.ts (or vercel.json):
// vercel.ts - recommended config (import from '@vercel/config'); vercel.json still works
import type { VercelConfig } from '@vercel/config/v1'
export const config: VercelConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 31536000, // cache optimized variants 1 year at the edge
    qualities: [50, 75],
  },
}

For remote images add remotePatterns. Store user uploads in Vercel Blob (public or private) rather than bundling them. Note: Vercel Postgres/KV are retired - use Marketplace storage (Neon Postgres, Upstash Redis) for data; this matters for LCP only insofar as a slow DB query delays TTFB (Step 6).

Step 4 - Bundle size: the #1 INP lever

INP is a JavaScript problem. Less client JS = a freer main thread = faster interactions. Measure before cutting:

# See exactly which routes ship how much First Load JS
ANALYZE=true npm run build         # with @next/bundle-analyzer wired in next.config
# or just read the build output table - the "First Load JS" column per route
npm run build

The senior moves, in order of payoff:

  1. Default to Server Components. Anything that does not need state, effects, or browser APIs should NOT have 'use client'. The biggest bundles are almost always an over-eager 'use client' at the top of a tree dragging children to the client. Push the directive down to the smallest interactive leaf.
  2. next/dynamic for heavy, below-the-fold, or interaction-gated client code (charts, editors, modals, maps):
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false, loading: () => <Skeleton /> })
  1. Kill heavy dependencies. A moment library, a full icon set imported as a namespace, or a 100KB date util is often the whole regression. Import single members, swap for lighter libs, or move the work server-side.
  2. Server-render data; do not ship a client fetch + spinner for content that could be in the initial HTML - that also helps LCP.

Re-run the build and confirm First Load JS for the route dropped. That delta is your evidence.

Step 5 - Fonts: a quiet LCP + CLS tax

Web fonts cause CLS (swap reflow) and delay LCP (text waits on the font). Use next/font - it self-hosts the font at build time (no third-party request), and auto-generates a fallback with size-adjust so the swap does not shift layout:

// app/layout.tsx
import { Inter } from 'next/font/local' // or next/font/google
const inter = Inter({ subsets: ['latin'], display: 'swap' })
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html lang="en" className={inter.className}>{/* ... */}</html>
}
  • Subset to the scripts you actually use; never ship the full multilingual file.
  • Self-hosting via next/font removes the render-blocking call to fonts.googleapis and the FOUT shift - both an LCP and a CLS win in one change.

Step 6 - Vercel caching: cut TTFB so LCP starts sooner

A slow LCP often starts with a slow TTFB - the function re-renders on every request. Serve from cache and the byte-zero arrives in milliseconds. Three layers, cheapest first:

  1. Static / ISR first. If the page can be prerendered, it should be. ISR works on Next.js, SvelteKit, Nuxt, and Astro. On Next.js 16 Cache Components, mark cacheable work with 'use cache' and tune lifetime/invalidation with cacheLife, cacheTag, and updateTag (these replace unstable_cache). The semantics live in vercel-edge-and-isr - defer the depth there; here you only confirm the page is not needlessly dynamic.

  2. CDN cache for dynamic responses via the three-tier header - Cache-Control for the browser, CDN-Cache-Control for the Vercel edge, Vercel-CDN-Cache-Control to override both at Vercel only:

// app/api/products/route.ts - fresh-feeling but edge-cached
export async function GET() {
  const data = await getProducts()
  return Response.json(data, {
    headers: {
      // browser: don't cache; edge: serve cached for 60s, revalidate in background
      'Cache-Control': 'public, max-age=0, must-revalidate',
      'Vercel-CDN-Cache-Control': 's-maxage=60, stale-while-revalidate=300',
    },
  })
}

stale-while-revalidate is the key to fast TTFB: users get an instant cached byte while Vercel refreshes in the background.

  1. Keep functions warm and lean. Fluid Compute already reuses instances to cut cold starts; help it by keeping the slow dependency (DB, AI call) off the critical render path or behind the cache above. If a page calls an LLM, route it through vercel-ai-gateway (GA, Aug 2025) so a slow provider can fail over instead of stalling TTFB - use plain provider/model strings via the AI SDK, not provider-specific packages.

Quality bar

A performance change is done only when all hold:

  • You captured a before number (field via Speed Insights / vercel metrics, or lab via Lighthouse on a production deploy) and an after number, and the after is better. No number, not done.
  • You fixed the metric that was actually red, routed through its real root cause - not a generic "make it faster" pass.
  • The LCP image (and only it) has priority; every next/image has width/height or a sized fill parent; sizes is set for responsive images.
  • Fonts go through next/font (self-hosted, subsetted, display: 'swap').
  • For any INP work, the route's First Load JS dropped in the build output and 'use client' sits at the smallest leaf that needs it.
  • Cacheable pages are static/ISR; genuinely dynamic ones set an explicit Vercel-CDN-Cache-Control with stale-while-revalidate.

Do NOT

  • Do NOT measure or benchmark against next dev. Dev disables minification, caching, and optimization - always test a production build / deployment.
  • Do NOT optimize without a profile. "It feels slow" is not a target; an LCP number is. If you have no measurement, go back to Step 1.
  • Do NOT put priority on more than the LCP image, and never use a raw <img> for a vital image when next/image is available.
  • Do NOT ship images without width/height - that is the most common CLS cause.
  • Do NOT reach for the edge runtime as a speed fix. Edge Functions are deprecated; default to Fluid Compute (Node.js 24) and let vercel-edge-and-isr own any runtime decision.
  • Do NOT cache-bust by disabling caching to "be safe" - a dynamic render on every request is usually the TTFB cause you are chasing.
  • Do NOT bolt on provider-specific AI SDKs for a slow model call - route through vercel-ai-gateway for fallbacks and observability.
  • Do NOT confuse the vitals: an INP fix (less JS) will not move LCP, and an LCP fix (image/cache) will not move INP. Diagnose first.

Calculator

Self-contained Node script. Save as vitals_triage.js and run with node vitals_triage.js. Edit the inputs with your measured numbers. No dependencies. It classifies each vital against the 2026 thresholds and prints an ordered fix list pointing at the right step.

// Core Web Vitals triage. Edit the measured numbers, then: node vitals_triage.js
const measured = {
  lcpSeconds: 4.8,   // Largest Contentful Paint, seconds (mobile field/lab)
  inpMs: 320,        // Interaction to Next Paint, milliseconds
  cls: 0.21,         // Cumulative Layout Shift, unitless
}

// 2026 "good" / "poor" thresholds (mobile).
const T = {
  lcp: { good: 2.5, poor: 4.0, unit: 's' },
  inp: { good: 200, poor: 500, unit: 'ms' },
  cls: { good: 0.1, poor: 0.25, unit: '' },
}

const fixes = {
  lcp: 'LCP: optimize the hero image (Step 3: next/image + priority + sizes), self-host fonts (Step 5), cut TTFB with cache/ISR (Step 6).',
  inp: 'INP: ship less client JS (Step 4: Server Components by default, push "use client" to leaves, next/dynamic heavy code, drop heavy deps).',
  cls: 'CLS: reserve space - width/height on every image (Step 3), next/font with size-adjust fallback (Step 5), no above-fold injection.',
}

function classify(value, t) {
  if (value <= t.good) return 'GOOD'
  if (value <= t.poor) return 'NEEDS WORK'
  return 'POOR'
}

// Severity: how far past "good", normalized, so the worst vital is fixed first.
function severity(value, t) {
  if (value <= t.good) return 0
  return (value - t.good) / (t.poor - t.good)
}

const rows = [
  ['lcp', measured.lcpSeconds, T.lcp],
  ['inp', measured.inpMs, T.inp],
  ['cls', measured.cls, T.cls],
].map(([key, value, t]) => ({
  key,
  value,
  unit: t.unit,
  status: classify(value, t),
  severity: severity(value, t),
}))

console.log('Core Web Vitals triage (mobile thresholds, 2026):')
for (const r of rows) {
  console.log(
    '  ' + r.key.toUpperCase().padEnd(4),
    String(r.value + r.unit).padEnd(8),
    r.status,
  )
}

const failing = rows
  .filter((r) => r.status !== 'GOOD')
  .sort((a, b) => b.severity - a.severity)

console.log('')
if (failing.length === 0) {
  console.log('All vitals are GOOD. Verify with field data before declaring done.')
} else {
  console.log('Fix in this order (worst first):')
  failing.forEach((r, i) => console.log('  ' + (i + 1) + '. ' + fixes[r.key]))
}

Worked example output

With the numbers above the script prints:

Core Web Vitals triage (mobile thresholds, 2026):
  LCP  4.8s     POOR
  INP  320ms    NEEDS WORK
  CLS  0.21     NEEDS WORK
Fix in this order (worst first):
  1. LCP: optimize the hero image (Step 3: next/image + priority + sizes), self-host fonts (Step 5), cut TTFB with cache/ISR (Step 6).
  2. CLS: reserve space - width/height on every image (Step 3), next/font with size-adjust fallback (Step 5), no above-fold injection.
  3. INP: ship less client JS (Step 4: Server Components by default, push "use client" to leaves, next/dynamic heavy code, drop heavy deps).

Read it: LCP is POOR and the furthest past "good", so the hero image and TTFB come first; CLS and INP are "needs work" and follow by severity. The script turns three raw numbers into an ordered, root-caused plan - and after each fix you re-measure (Step 1) and re-run it to confirm the status flipped to GOOD.

Template: perf-audit-record

Copy this, fill the BEFORE column from Step 1, do the work, fill AFTER, and keep it in the PR description so the win is auditable.

NEXT-ON-VERCEL PERF AUDIT. [FILL: route / page]. [FILL: date]

MEASUREMENT SOURCE
  Field (Speed Insights / vercel metrics):  [FILL: yes/no]
  Lab (Lighthouse, production deploy):      [FILL: yes/no]

VITAL        BEFORE        TARGET     AFTER       FIX APPLIED (step)
  LCP        [FILL]s       <= 2.5s    [FILL]s     [FILL: e.g. priority hero + sizes (3)]
  INP        [FILL]ms      <= 200ms   [FILL]ms    [FILL: e.g. dynamic-import chart (4)]
  CLS        [FILL]        <= 0.1     [FILL]      [FILL: e.g. width/height + next/font (5)]
  TTFB       [FILL]ms      [target]   [FILL]ms    [FILL: e.g. SWR CDN cache (6)]

BUNDLE (if INP touched)
  First Load JS for route, before:  [FILL] KB
  First Load JS for route, after:   [FILL] KB

VERDICT: [FILL: all targets met / remaining red metric + next step]

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.