JavaScript is expensive to download and run — split it, import only what you use, compress it, and hint the browser early.
Why: unlike an image, JavaScript isn’t just downloaded — the browser must also read and run it, which ties up the page. The less you ship, the faster the page becomes usable. Note: this is the main lever behind INP (how fast the page responds to clicks).
Why: by default your whole app downloads up front, even code for screens the user may never open. A dynamic import() loads a chunk only when it’s needed (e.g. when a dialog opens), shrinking the initial download. Note: this is called "code-splitting" — breaking one big bundle into pieces loaded on demand.
// Loaded immediately (small):
button.addEventListener('click', async () => {
// The chart library downloads only when the button is clicked
const { renderChart } = await import('./chart.js')
renderChart()
})Why: importing a whole library when you need one function ships code you never call. Importing the specific function lets the bundler drop the rest — a process called "tree-shaking" (shaking the dead leaves off the tree). Note: this only works with named imports from libraries that support it.
// ❌ Pulls in the entire library
import _ from 'lodash'
_.debounce(fn, 200)
// ✅ Pulls in just debounce; the rest is tree-shaken away
import debounce from 'lodash/debounce'
debounce(fn, 200)Why: resource hints let you start important work sooner. preconnect opens a connection to another domain ahead of time; preload downloads a critical file early; prefetch grabs something the user will likely need next. Note: use these sparingly — hinting everything cancels the benefit.
<!-- Warm up a connection to an API or font host -->
<link rel="preconnect" href="https://api.example.com" />
<!-- Download a file the first screen needs right now -->
<link rel="preload" href="/hero.avif" as="image" />
<!-- Grab a page the user will probably visit next -->
<link rel="prefetch" href="/dashboard.js" />