Import on Interaction: Load When User Interacts
Import on interaction is a practical frontend performance pattern built on dynamic import(). Instead of eagerly shipping all application code, you defer optional feature bundles until a real user action suggests the code is needed.
This improves startup performance by reducing the amount of JavaScript that must be downloaded, parsed, compiled, and executed on the initial page load.
The tradeoff is simple: you save initial cost, but the first interaction may now pay a loading cost. Good implementations manage that tradeoff with caching, prefetching, loading UI, and careful candidate selection.
Quick Navigation: What Import on Interaction Means • Basic Click Pattern • Hover or Focus as Intent Signals • Caching the Loaded Module • Good vs Bad Candidates • Tradeoffs and UX Costs • React Patterns • Real-World Examples
Quick Decision Guide
Senior-Level Decision Guide:
- Use import on interaction for optional, non-critical, heavy features. - Trigger loading on click for correctness, or on hover/focus for earlier intent. - Cache the loaded Promise or module so you do not repeat work. - Always handle loading and failure states. - Do not defer code that is required for the initial experience.
Interview framing: Import on interaction is an intent-driven code-splitting strategy that reduces initial JavaScript cost by loading secondary features only when users are likely to need them.
What Import on Interaction Means
Core Idea
With dynamic imports, you can load code at runtime:
const module = await import('./HeavyFeature');Instead of importing that module during initial page load, you wait until the user interacts with a trigger.
Examples:
Open FiltersOpen ChatWhy this helps
If the feature is not used by every visitor, there is no reason to force all users to pay its JavaScript cost up front.
This is especially valuable for:
Basic Click Pattern
Load on Click
The simplest version loads a module on the first click and reuses it afterward.
function SettingsButton() {
const [Modal, setModal] = useState(null);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const handleOpen = async () => {
if (!Modal) {
setLoading(true);
try {
const mod = await import('./SettingsModal');
setModal(() => mod.default);
} finally {
setLoading(false);
}
}
setOpen(true);
};
return (
<>
<button onClick={handleOpen}>
{loading ? 'Loading…' : 'Open Settings'}
</button>
{open && Modal && <Modal onClose={() => setOpen(false)} />}
</>
);
}Interview takeaway
This is the cleanest mental model:
Hover or Focus as Intent Signals
Prefetch Before Click
Sometimes waiting until the exact click makes the UI feel delayed.
A common improvement is to start loading when the user shows intent slightly earlier:
mouseenterfocustouchstart in some mobile flowsfunction ChatTrigger() {
const moduleRef = useRef(null);
const promiseRef = useRef(null);
const prefetch = () => {
if (!promiseRef.current) {
promiseRef.current = import('./ChatWidget').then(mod => {
moduleRef.current = mod.default;
return mod;
});
}
};
const handleClick = async () => {
if (!moduleRef.current) {
await prefetch();
}
moduleRef.current?.open();
};
return (
<button onMouseEnter={prefetch} onFocus={prefetch} onClick={handleClick}>
Open Chat
</button>
);
}Why this works
Hover and focus often happen a fraction of a second before click. That small window can hide some or all of the network and parse cost.
Caching the Loaded Module
Avoid Duplicate Loading Logic
One of the most important implementation details is caching.
Even though module loaders typically avoid re-downloading the same chunk after it is resolved, your UI code should still avoid repeatedly creating awkward loading flows.
A good pattern is to cache either:
let modalPromise;
function loadModal() {
if (!modalPromise) {
modalPromise = import('./Modal');
}
return modalPromise;
}Why caching the Promise is useful
If a user clicks twice quickly, both calls can share the same in-flight Promise instead of starting separate control flow paths.
Good vs Bad Candidates
Good Candidates
Import on interaction works well for features that are:
Examples:
Bad Candidates
Avoid this pattern for code that is:
Examples:
Tradeoffs and UX Costs
The Main Tradeoff
Import on interaction does not remove cost. It moves cost from initial load to first use.
That is usually good when the feature is optional, but it introduces a new UX question:
> Is the first interaction still fast enough?
Typical UX issues
How to reduce that cost
React Patterns
Reusable Hook
A reusable hook can centralize loading, caching, and loading state.
function useLoadOnInteraction(loader) {
const promiseRef = useRef(null);
const [Component, setComponent] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const load = useCallback(async () => {
if (Component) return Component;
if (!promiseRef.current) {
setIsLoading(true);
setError(null);
promiseRef.current = loader()
.then(mod => {
const resolved = mod.default || mod;
setComponent(() => resolved);
return resolved;
})
.catch(err => {
promiseRef.current = null;
setError(err);
throw err;
})
.finally(() => {
setIsLoading(false);
});
}
return promiseRef.current;
}, [loader, Component]);
const prefetch = useCallback(() => {
if (!promiseRef.current && !Component) {
void load();
}
}, [load, Component]);
return { Component, isLoading, error, load, prefetch };
}Why this is a strong interview answer
It shows:
Real-World Examples
Example 1: Rich Text Editor
A comment box may start as a lightweight textarea, then load the rich editor only on focus.
function CommentBox() {
const { Component: Editor, load, isLoading } = useLoadOnInteraction(
() => import('./RichTextEditor')
);
const [active, setActive] = useState(false);
const handleFocus = async () => {
setActive(true);
await load();
};
return active && Editor ? (
<Editor />
) : (
<textarea
onFocus={handleFocus}
placeholder={isLoading ? 'Loading editor…' : 'Write a comment...'}
/>
);
}Example 2: Video Player
Video thumbnails can defer the player library until the user actually presses play.
Example 3: Advanced Filters
Filter drawers in dashboards are often good candidates because many users never open them.
Example 4: Auth Modal
Sign-in flows can be split if the landing page does not need auth logic immediately.
Performance Impact
Why It Improves Performance
Reducing initial JavaScript helps in multiple ways:
Important nuance
This pattern is most valuable when the deferred module is meaningfully large or costly.
If a feature is tiny, splitting it may add complexity and extra request overhead without much real gain.
What to measure
You should validate impact with real profiling:
A strong engineering answer always includes measurement, not just theory.
Best Practices
1. Cache in-flight and resolved modules
Do not rebuild the same loading logic every interaction.
2. Use early intent signals carefully
Hover and focus can improve responsiveness, but do not over-prefetch every possible feature.
3. Provide clear loading states
If a click now triggers code loading, the user needs visible feedback.
4. Handle failures
Dynamic imports can fail because of network issues or chunk mismatches.
5. Split by meaningful feature boundaries
Do not create dozens of tiny fragments unless they reflect real usage boundaries.
6. Keep critical paths eager
Defer optional experiences, not the core page shell.
Common Pitfalls
Pitfall: loading on mount instead of interaction
useEffect(() => {
import('./Module');
}, []);That is lazy-on-mount, not import on interaction.
Pitfall: no loading feedback
A user clicks and nothing appears to happen.
Pitfall: deferring essential logic
The site may benchmark better but feel worse.
Pitfall: ignoring mobile intent patterns
Hover does not exist on many touch devices, so click or focus-based strategies matter more there.
Interview Scenarios
Scenario 1: Why not lazy load everything?
Because some code is needed immediately, and too much fragmentation can hurt user experience.
Scenario 2: When is hover-prefetch better than click-only?
When you want to reduce first-use latency and the hover is a strong signal of likely activation.
Scenario 3: What is the main tradeoff?
You reduce startup cost but may add first-interaction latency.
Scenario 4: How would you explain this in one sentence?
Import on interaction is an intent-based code-splitting strategy that loads optional code only when the user is likely to need it.
Scenario 5: What makes a good candidate?
A heavy, optional feature triggered by explicit user action, such as a modal, editor, or chat widget.