Load non-critical code on user interaction
Defer JavaScript modules, widgets, and third-party code until the user signals intent through a click, focus, hover, or similar interaction.
- Move non-essential code behind clicks, focus, hovers, or explicit user actions
- Use dynamic import() so the code is absent from the initial route bundle
- Show immediate UI feedback while the deferred chunk loads
- Use this for chat widgets, editors, maps, filters, and export flows that are not needed on first paint
Rule Details
Import-on-interaction delays code loading until the user does something that proves the feature is relevant. It is especially useful for features that are expensive but optional on the initial view.
Code Examples
Plain JavaScript Dynamic Import
// ❌ Bad: Heavy code is loaded for every visitor
import { openProductConfigurator } from './product-configurator.js'
document.querySelector('#configure').addEventListener('click', () => {
openProductConfigurator()
})
// ✅ Good: Load only when the user asks for it
document.querySelector('#configure').addEventListener('click', async () => {
showConfiguratorSkeleton()
const { openProductConfigurator } = await import('./product-configurator.js')
openProductConfigurator()
})Load a Third-Party Widget After Intent
const chatButton = document.querySelector('#open-chat')
chatButton.addEventListener('click', async () => {
chatButton.disabled = true
chatButton.textContent = 'Loading chat...'
const { bootChat } = await import('./chat-loader.js')
await bootChat()
})React Example
import { useState } from 'react'
export function ExportButton() {
const [loading, setLoading] = useState(false)
async function handleExport() {
setLoading(true)
const { exportReport } = await import('./export-report')
await exportReport()
setLoading(false)
}
return (
<button onClick={handleExport} disabled={loading}>
{loading ? 'Preparing export...' : 'Export report'}
</button>
)
}Why It Matters
- Smaller initial bundles: Users do not download feature code they may never use.
- Less main-thread work: Deferring parse and execution work reduces competition with critical rendering and input handling.
- Better prioritisation: The browser can focus on content, LCP resources, and essential app logic before optional features.
- Cleaner third-party strategy: Widgets like chat, reviews, maps, and rich editors often belong behind explicit intent instead of first paint.
When to Use It
Use import-on-interaction for:
- Chat widgets
- Rich text editors
- Map embeds
- Export and print flows
- Advanced filters, configurators, and comparison tools
Avoid it for:
- Navigation needed on the first screen
- The primary purchase or sign-in action
- Above-the-fold media or content needed for LCP
- Code required before the first user interaction can succeed
Verification
Use Chrome DevTools Coverage (opens in new tab) or a waterfall trace to confirm the deferred module is actually absent from the initial path, not just moved to a different eager bundle.
- Confirm the deferred module is absent from the initial route bundle or first-load network waterfall.
- Trigger the interaction and verify the import request starts immediately and the UI shows a loading state within roughly
100ms. - Re-test the same flow and confirm repeat interactions reuse the cached chunk without another full download.
- Measure the route before and after the change and confirm the initial JavaScript cost or main-thread work decreases.
- Verify the interaction still feels responsive on a throttled mobile profile and does not create new long tasks above
50msbefore the deferred feature starts loading.
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Inspect this route or component for heavy modules, widgets, or third-party scripts that load on first render but are only needed after a user clicks, focuses, hovers, or opens a panel.
Fix
Auto-fix issues
Move the non-critical dependency behind a user-triggered dynamic import(), add immediate loading feedback, and ensure the initial render still works without the deferred feature.
Explain
Learn more
Explain import-on-interaction, why it improves initial performance, and which features are safe to defer until the user shows intent.
Review
Code review
Inspect event handlers, modal triggers, drawers, editors, maps, export actions, and third-party widgets. Flag code that is included in the initial bundle even though it is only needed after a user interaction, and verify the fix keeps the initial path lighter without degrading the triggered flow.