How do you implement a lazy loading pattern for modules?
Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.
Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.
Lazy loading defers module loading until they're actually needed, reducing initial bundle size and improving load time.
Dynamic import (ES modules):
// Traditional import - loaded immediately
import { heavyFunction } from './heavy-module.js';
// Dynamic import - loaded on demand
async function loadModule() {
const module = await import('./heavy-module.js');
module.heavyFunction();
}
// Or with destructuring
const { heavyFunction } = await import('./heavy-module.js');
Route-based lazy loading (React):
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</Router>
);
}
Feature-based lazy loading:
class FeatureLoader {
constructor() {
this.features = new Map();
}
async loadFeature(name) {
if (this.features.has(name)) {
return this.features.get(name);
}
const module = await import(`./features/${name}.js`);
this.features.set(name, module);
return module;
}
}
const loader = new FeatureLoader();
// Load feature only when needed
button.addEventListener('click', async () => {
const feature = await loader.loadFeature('advanced-charts');
feature.render(data);
});
Intersection Observer lazy loading:
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
const moduleName = entry.target.dataset.module;
const module = await import(`./modules/${moduleName}.js`);
module.init(entry.target);
observer.unobserve(entry.target);
}
});
});
// Observe elements
document.querySelectorAll('[data-module]').forEach(el => {
observer.observe(el);
});
Conditional lazy loading:
async function loadEditor() {
if (window.innerWidth > 768) {
// Load full editor for desktop
const { FullEditor } = await import('./FullEditor.js');
return new FullEditor();
} else {
// Load mobile editor
const { MobileEditor } = await import('./MobileEditor.js');
return new MobileEditor();
}
}
Prefetching for better UX:
// Prefetch on hover
link.addEventListener('mouseenter', () => {
import(/* webpackPrefetch: true */ './heavy-module.js');
});
// Preload critical modules after initial load
window.addEventListener('load', () => {
setTimeout(() => {
import(/* webpackPreload: true */ './important-module.js');
}, 1000);
});
Module caching:
class ModuleCache {
constructor() {
this.cache = new Map();
this.pending = new Map();
}
async load(path) {
// Return cached module
if (this.cache.has(path)) {
return this.cache.get(path);
}
// Return pending promise
if (this.pending.has(path)) {
return this.pending.get(path);
}
// Load module
const promise = import(path).then(module => {
this.cache.set(path, module);
this.pending.delete(path);
return module;
});
this.pending.set(path, promise);
return promise;
}
}
const moduleCache = new ModuleCache();
const module = await moduleCache.load('./module.js');
Webpack code splitting:
// Magic comments for webpack
const module = await import(
/* webpackChunkName: "my-chunk" */
/* webpackMode: "lazy" */
'./module.js'
);
Candidate Response Strategy & Interview Tips
- Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
- Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
- Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
- Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.