JavaScript Medium technical 1 views 2 min read

How do you implement a lazy loading pattern for modules?

Peer-reviewed by HireXTech Technical Panel Updated for 2025/2026 hiring Editorial standards
Practise this track
Interviewer Expectations for this Question
01
Core Competency

Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.

02
Evaluation Criteria

Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.

Comprehensive Model Answer Verified Solution

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

  1. Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
  2. Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
  3. Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
  4. Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.
Related Topics & Skills
Spotted an error or have an alternative solution?