Built-in Components — Image, Font, Script, Link, and Form13 min read
Module F-6·13 min read
Why next/image exists, the four Script loading strategies, the new Form component for prefetched search, and what Link's prefetch prop actually controls.
Who this is for: Developers who have the routing and data fetching fundamentals from F-2 through F-5. This module covers the performance-focused built-in components that Next.js provides to solve specific web performance problems that raw HTML handles poorly. Each one exists because the naive implementation has a measurable negative impact on Core Web Vitals.
Every component in this module replaces something you've almost certainly done the naive way — and the naive way works, in the sense that it renders. But each naive approach has a specific performance pathology that shows up in real metrics.
Unoptimised images: Largest Contentful Paint catastrophe. Raw @import fonts: FOUT (Flash of Unstyled Text) and layout shift. Synchronous third-party <script> tags: parser blocking and slower Time to Interactive. Regular <a> tags: no prefetching, no client-side navigation.
Next.js's built-in components solve these problems systematically so you don't have to rediscover them project by project. Understanding why each one works the way it does means you'll use them correctly instead of fighting them.
When you don't know the image dimensions (dynamic content, CMS images), use fill to let the image fill its parent container:
<div className="relative h-64 w-full"> <Image src={product.imageUrl} alt={product.name} fill className="object-cover" // ← CSS determines how image fits the container sizes="(max-width: 768px) 100vw, 50vw" // ← tells Next.js which sizes to generate /> </div>
The parent must have position: relative (or absolute/fixed). The sizes attribute is important — it tells the browser (and Next.js's image optimizer) which image size to serve at each breakpoint. Without it, Next.js defaults to 100vw, potentially serving much larger images than needed.
By default, images are lazy-loaded. The one exception: images that are visible immediately on page load — your hero image, your above-the-fold product photo — should have priority. This tells Next.js to preload them with <link rel="preload"> in the document <head>, improving LCP.
// Above the fold — use priority <Image src={heroImage} alt="Hero" priority />
// Below the fold — lazy loading is correct (default) <Image src={productImage} alt={product.name} />
LCP (Largest Contentful Paint) is Google's primary Core Web Vitals metric. The LCP element is almost always a hero image or above-the-fold photo. Forgetting priority on the LCP image is one of the most common Next.js performance mistakes — your Lighthouse score will tell you if you've missed it.
Fonts are a well-documented performance problem. The traditional approach — @import url('https://fonts.google.com/...') — blocks rendering, can cause Flash of Unstyled Text, and triggers Cumulative Layout Shift when the font loads and text reflows.
next/font solves this by downloading the font at build time, self-hosting it, and generating CSS with font-display: swap and fallback metrics calculated to match the Google font's actual glyph shapes.
const inter = Inter({ subsets: ['latin'], display: 'swap', // ← show fallback while font loads, then swap variable: '--font-inter', // ← CSS custom property for use in Tailwind });
Block rendering to avoid CLS (a different but equally bad trade-off)
Carefully calculate size-adjust, ascent-override, and descent-override CSS properties by hand to match your fallback font's metrics
next/font does option 3 automatically, at build time, with zero manual CSS. The fallback font is sized and positioned to match the final font so precisely that the swap is visually imperceptible. In practice, your layout doesn't shift at all.
Raw <script> tags in HTML block the parser. Every millisecond your analytics, chat widget, or A/B testing SDK spends loading is a millisecond the browser isn't rendering your content.
next/script gives you explicit control over when third-party scripts load:
beforeInteractive — loads and executes before the page is interactive. Use this only for scripts that must be available before the user can interact (consent managers, critical A/B testing that affects above-the-fold content). This strategy blocks hydration.
afterInteractive (default) — loads after the page becomes interactive. Correct for most analytics, chat widgets, and non-critical third parties. The page is usable immediately; the script loads in the background.
lazyOnload — loads during browser idle time, lowest priority. Use for scripts that have no urgency whatsoever — social sharing widgets, survey tools, anything that can wait until the page is fully loaded and the user is reading.
worker (experimental) — moves script execution to a Web Worker, keeping the main thread free. Use for heavy scripts that do significant computation. Currently requires enabling nextScriptWorkers: true in next.config.ts.
You've used <Link> since F-5. This module adds the details that matter for performance.
When a <Link> enters the viewport, Next.js starts fetching the destination route in the background. For static routes, this is a fetch of the pre-rendered HTML. For dynamic routes, it fetches the RSC payload (the serialized Server Component output). By the time the user clicks, the content is cached — navigation appears instant.
The prefetch cache lives in the React Cache and is separate from the HTTP cache. It's ephemeral — it survives for the duration of the session but not across page reloads.
import Link from 'next/link';
// Renders as <a href="/blog"> — standard HTML anchor for accessibility and crawlers <Link href="/blog">Blog</Link>
// Programmatic state alongside navigation <Link href="/checkout" className="btn-primary"> Checkout </Link>
// External links — use regular <a>, not Link <a href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer"> GitHub </a>
Use <Link> for internal navigation. Use <a> for external links. The difference matters: <Link> triggers client-side navigation (fast, no full page reload). <a> does a full browser navigation (reloads the page, loses React state).
Next.js 15 introduced a <Form> component from next/form that extends the HTML <form> with two capabilities: soft navigation for GET forms and route prefetching on focus.
When the user submits this form, instead of a full page reload to /search?q=value, Next.js does a client-side navigation — the same as router.push('/search?q=value'). The URL updates, the search results page renders via RSC, and React state outside the search results is preserved.
On focus of the search input, <Form> also prefetches the /search route in the background. By the time the user finishes typing and submits, the route's server component shell is already cached.
For POST forms (mutations), use Server Actions instead of <Form> — that's covered in P-2.
// Only use next/form for GET forms (search, filters, navigation) // For POST forms (create/update/delete), use Server Actions with regular <form>
Here's how these components compose in practice — a product listing page that gets most things right by default:
// app/products/page.tsx (Server Component)
import Image from 'next/image'; import Link from 'next/link'; import Form from 'next/form'; import { Suspense } from 'react'; import FilterBar from '@/components/FilterBar';
Forgetting sizes on fill images. Without sizes, Next.js generates an image at 100vw width even when the image occupies 25% of the screen. For a 4-column grid, the correct sizes is 25vw (or more accurately, (max-width: 768px) 50vw, 25vw to handle the 2-column mobile layout).
Using <img> instead of <Image> for remote images you control. If you're serving images from S3, Cloudinary, or your own CDN and want optimization, use next/image. If you're linking to external images you can't control (user avatars, third-party logos), you can use <img> with unoptimized if optimization isn't needed.
Putting <Script strategy="beforeInteractive"> on everything. Every beforeInteractive script delays hydration. Use it only for scripts that must exist before the first interaction — typically just consent managers. Everything else is afterInteractive or lazyOnload.
Loading Google Fonts with @import in CSS. Once you know next/font exists, there's no reason to use @import. The performance difference is measurable in LCP and CLS scores.
F-7 covers Route Handlers — building API endpoints inside Next.js with the route.ts file convention, proxying external APIs, and when a Route Handler is the right tool versus a Server Action.
After F-7 comes F-8 — the capstone module where you build a complete content site end-to-end, putting everything from F-1 through F-7 into practice in a real application before moving into the Practitioner phase.
Knowledge Check
Which next/script strategy should be used for scripts that have no urgency whatsoever, such as social sharing widgets?
What are the two main capabilities introduced by the Next.js 15 <Form> component (next/form) for GET forms?
What is the consequence of forgetting to include the sizes prop on a next/image component with the fill prop?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.