Asset Loading - Preload fonts, scripts, and stylesheets with React DOM APIs
Recipe
import { prefetchDNS, preconnect, preload, preinit } from "react-dom";
function App() {
// Eagerly load and execute a script
preinit("https://cdn.example.com/analytics.js", { as: "script" });
// Preload a font (download early, use later)
preload("https://fonts.example.com/inter.woff2", {
as: "font",
type: "font/woff2",
crossOrigin: "anonymous",
});
// Preload a stylesheet
preload("https://cdn.example.com/theme.css", { as: "style" });
// DNS prefetch for a domain you will request later
prefetchDNS("https://api.example.com");
// Preconnect (DNS + TCP + TLS) for a domain you will request soon
preconnect("https://images.example.com");
return <main>{/* ... */}</main>;
}When to reach for this: Use these APIs to improve initial page load and client-side navigation performance by telling the browser about resources it will need before React renders the components that use them.
Working Example
// A page that preloads all critical resources for optimal performance
import { Suspense } from "react";
import { preload, preinit, preconnect } from "react-dom";
// Preload critical resources at the module level (runs once on import)
preconnect("https://api.myapp.com");
preload("/fonts/brand-bold.woff2", { as: "font", type: "font/woff2", crossOrigin: "anonymous" });
preload("/fonts/brand-regular.woff2", { as: "font", type: "font/woff2", crossOrigin: "anonymous" });
function CriticalCSS() {
return (
<>
{/* React deduplicates and manages stylesheet loading order via precedence */}
<link rel="stylesheet" href="/styles/reset.css" precedence="reset" />
<link rel="stylesheet" href="/styles/base.css" precedence="default" />
<link rel="stylesheet" href="/styles/theme.css" precedence="high" />
</>
);
}
function HeavyWidget() {
// Preinit a script needed by this widget
preinit("/scripts/chart-library.js", { as: "script" });
return (
<div className="widget">
<h2>Sales Dashboard</h2>
<div id="chart-container" />
</div>
);
}
function ImageGallery({ images }: { images: { src: string; alt: string }[] }) {
// Preload the first few images for instant display
images.slice(0, 3).forEach((img) => {
preload(img.src, { as: "image" });
});
return (
<div className="grid grid-cols-3 gap-4">
{images.map((img, i) => (
<img
key={i}
src={img.src}
alt={img.alt}
loading={i < 3 ? "eager" : "lazy"}
/>
))}
</div>
);
}
export default function DashboardPage() {
return (
<>
<CriticalCSS />
<title>Dashboard | MyApp</title>
<header>
<h1>Dashboard</h1>
</header>
<Suspense fallback={<p>Loading widget...</p>}>
<HeavyWidget />
</Suspense>
<Suspense fallback={<p>Loading gallery...</p>}>
<ImageGallery
images={[
{ src: "/images/product-1.jpg", alt: "Product 1" },
{ src: "/images/product-2.jpg", alt: "Product 2" },
{ src: "/images/product-3.jpg", alt: "Product 3" },
{ src: "/images/product-4.jpg", alt: "Product 4" },
{ src: "/images/product-5.jpg", alt: "Product 5" },
{ src: "/images/product-6.jpg", alt: "Product 6" },
]}
/>
</Suspense>
</>
);
}What this demonstrates:
preconnectestablishing early connections to API domainspreloadfor fonts at the module level (before any component renders)<link rel="stylesheet" precedence={}>for ordered, deduplicated CSSpreiniteagerly loading a third-party script inside a componentpreloadfor images to speed up above-the-fold rendering- Suspense boundaries around heavy components for streaming
Deep Dive
How It Works
preload(href, options)-- Inserts a<link rel="preload">tag into the document. The browser downloads the resource at high priority but does not execute or apply it. Use for fonts, images, stylesheets, and scripts you will need soon.preinit(href, options)-- Downloads and executes/applies the resource immediately. For scripts, this means the script runs. For stylesheets, this means the CSS is applied. Use when you need the resource active right away.prefetchDNS(href)-- Inserts a<link rel="dns-prefetch">hint. The browser resolves the domain name early. Cheapest option -- use when you might request from this domain.preconnect(href)-- Inserts a<link rel="preconnect">. The browser performs DNS + TCP + TLS handshake early. Use when you will definitely request from this domain soon.- All four APIs can be called anywhere -- inside components, event handlers, module scope, or even effects. React deduplicates calls with the same
href. - During SSR, these calls emit the appropriate
<link>tags in the HTML<head>and as early hints (103 Early Hints) if the server supports it. - Stylesheet Suspense --
<link rel="stylesheet" precedence={}>tells React to manage the stylesheet. React will suspend rendering of the component until the stylesheet loads, preventing a flash of unstyled content.
Variations
Preloading on hover (predictive):
"use client";
import { preload } from "react-dom";
import Link from "next/link";
function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
function handleMouseEnter() {
// Preload the page's CSS and hero image when user hovers
preload(`/styles${href}.css`, { as: "style" });
preload(`/images${href}-hero.jpg`, { as: "image" });
}
return (
<Link href={href} onMouseEnter={handleMouseEnter}>
{children}
</Link>
);
}Conditional preloading based on viewport:
"use client";
import { useEffect } from "react";
import { preload } from "react-dom";
function AdaptiveLoader() {
useEffect(() => {
const connection = (navigator as any).connection;
if (connection?.effectiveType === "4g") {
// Only preload heavy resources on fast connections
preload("/videos/hero.mp4", { as: "video" });
preload("/fonts/display.woff2", { as: "font", crossOrigin: "anonymous" });
}
}, []);
return null;
}Module-level preloading (runs on import):
import { preconnect, preload } from "react-dom";
// These execute when the module is imported, before any component renders
preconnect("https://cdn.example.com");
preload("https://cdn.example.com/critical.css", { as: "style" });
export default function Page() {
return <div>Content that needs cdn.example.com resources</div>;
}TypeScript Notes
- All four functions are imported from
"react-dom". preload(href: string, options: { as: string; crossOrigin?: string; type?: string; fetchPriority?: "high" | "low" | "auto" }).preinit(href: string, options: { as: "script" | "style"; crossOrigin?: string; fetchPriority?: "high" | "low" | "auto"; nonce?: string }).prefetchDNS(href: string)andpreconnect(href: string, options?: { crossOrigin?: string })have minimal options.- The
precedenceattribute on<link rel="stylesheet">is typed asstring.
Gotchas
- Preloading too many resources -- Browsers have limits on concurrent connections. Preloading everything is counterproductive. Fix: Only preload resources that are critical for the above-the-fold experience.
preinitexecutes scripts immediately -- If the script has side effects or depends on DOM elements that have not rendered yet, it may fail. Fix: Usepreloadinstead and add a<script>tag when the DOM is ready, or ensure the script handles missing DOM gracefully.- Font preload without crossOrigin -- Fonts loaded via
preloadmust specifycrossOrigin: "anonymous"even for same-origin fonts, per the Fetch spec. Fix: Always includecrossOrigin: "anonymous"for font preloads. - Stylesheet without
precedenceis not managed -- If you omitprecedencefrom a<link rel="stylesheet">, React does not deduplicate or suspend for it. Fix: Addprecedenceto all stylesheet links you want React to manage. - SSR vs client preload timing -- During SSR, preload hints appear in the initial HTML. During client navigation, they are injected dynamically. The timing difference may affect performance characteristics. Fix: Test both SSR and client navigation paths.
Alternatives
| Approach | When to choose |
|---|---|
| React DOM preload APIs | React 19 built-in, works with SSR and client rendering |
HTML <link> in <head> template | Static resources known at build time |
Next.js <Script> component | Next.js projects needing script loading strategies |
@next/font / next/font | Next.js font optimization with automatic subsetting |
Manual document.createElement("link") | Rare cases needing programmatic control outside React |
| Resource hints via HTTP headers | Server-level optimization (103 Early Hints) |
FAQs
What is the difference between preload and preinit?
preloaddownloads the resource at high priority but does not execute or apply itpreinitdownloads and executes/applies the resource immediately (scripts run, CSS is applied)- Use
preloadfor resources you will need soon; usepreinitwhen you need them active right away
What is the difference between prefetchDNS and preconnect?
prefetchDNSonly resolves the domain name (DNS lookup) -- cheapest optionpreconnectperforms DNS + TCP + TLS handshake -- more expensive but saves more time- Use
prefetchDNSwhen you might request from a domain; usepreconnectwhen you definitely will soon
Where can these asset loading APIs be called?
- Anywhere: inside components, event handlers, module scope, or effects
- React deduplicates calls with the same
href - Module-level calls execute when the module is imported, before any component renders
How do you preload fonts correctly?
preload("/fonts/brand.woff2", {
as: "font",
type: "font/woff2",
crossOrigin: "anonymous", // required for all fonts
});You must include crossOrigin: "anonymous" even for same-origin fonts, per the Fetch spec.
How do you preload resources on hover for faster navigation?
function NavLink({ href, children }) {
function handleMouseEnter() {
preload(`/styles${href}.css`, { as: "style" });
preload(`/images${href}-hero.jpg`, { as: "image" });
}
return (
<a href={href} onMouseEnter={handleMouseEnter}>
{children}
</a>
);
}How does stylesheet Suspense work with the precedence attribute?
<link rel="stylesheet" precedence="default">tells React to manage the stylesheet- React suspends rendering of the component until the stylesheet loads, preventing a flash of unstyled content
- Stylesheets with the same
precedencevalue are grouped together in<head>
How do these APIs behave during SSR vs client-side navigation?
- During SSR, calls emit
<link>tags in the HTML<head>and as 103 Early Hints if the server supports it - During client navigation, they are injected dynamically into the DOM
- Test both SSR and client navigation paths as timing differences may affect performance
Can you conditionally preload based on network speed?
useEffect(() => {
const conn = (navigator as any).connection;
if (conn?.effectiveType === "4g") {
preload("/videos/hero.mp4", { as: "video" });
preload("/fonts/display.woff2", {
as: "font",
crossOrigin: "anonymous",
});
}
}, []);Gotcha: Why is preloading too many resources counterproductive?
- Browsers have limits on concurrent connections per domain
- Preloading everything competes for bandwidth and can actually slow down critical resources
- Only preload resources that are critical for the above-the-fold experience
Gotcha: My preinit script fails because the DOM is not ready. What should I do?
preinitexecutes scripts immediately, which may fail if the script depends on DOM elements that have not rendered- Use
preloadinstead and add a<script>tag when the DOM is ready - Or ensure the script handles missing DOM elements gracefully
How are the asset loading APIs typed in TypeScript?
preload(href: string, options: {
as: string;
crossOrigin?: string;
type?: string;
fetchPriority?: "high" | "low" | "auto";
}): void;
preinit(href: string, options: {
as: "script" | "style";
crossOrigin?: string;
fetchPriority?: "high" | "low" | "auto";
nonce?: string;
}): void;All four functions are imported from "react-dom".
What is the TypeScript type for the precedence attribute on stylesheet links?
- The
precedenceattribute is typed asstringon<link rel="stylesheet"> - It is a React-specific extension not present in standard HTML typings
- Any string value works; common choices are
"reset","default", and"high"
Related
- Metadata -- Rendering title, meta, and link tags in components
- Server Components -- Preloading resources during server rendering
- Overview -- Full list of React 19 features