Performance Best Practices

Optimize bundle size and loading performance with the UMD Design System

Introduction

The UMD Design System provides multiple strategies for optimizing bundle size and loading performance. By leveraging ES modules, tree-shaking, and strategic code splitting, you can significantly reduce initial load times and improve the user experience of your applications.

This guide covers the design system's export strategy, bundling configuration with Vite, and production-ready patterns for dynamic component loading. Whether you're building a simple static site or a complex web application, these patterns will help you achieve optimal performance while maintaining code maintainability.

The design system is built with performance in mind, providing granular control over what gets loaded and when. Components are organized into logical groups that align with common usage patterns, allowing you to load only what you need, when you need it. Note: Grouped exports (structural, content, interactive, feed) were introduced in Release 1.14.0.

📦 Alternative Loading Methods: While this guide focuses on performance optimization through code splitting, simpler options are available for prototyping and development:
  • CDN Usage - Quick setup via unpkg for prototypes (see Getting Started)
  • Bundle Import - Single import with all packages included via @universityofmaryland/web-components-library/bundle
For production applications, we recommend the code splitting approaches detailed below.

Export Strategy

The component library provides two complementary export strategies to optimize bundle size and loading performance. Choose the approach that best fits your application's needs.

Grouped Exports

The design system provides pre-configured component groups based on common usage patterns. This approach balances convenience with performance.

// Structural Components - Page layout and navigation
// Typically loaded first for above-the-fold content
import LoadStructuralComponents from '@universityofmaryland/web-components-library/structural';
LoadStructuralComponents(); // Loads: actions, hero, navigation

// Content Components - Display components for information
// Load after structural components for main content
import LoadContentComponents from '@universityofmaryland/web-components-library/content';
LoadContentComponents(); // Loads: card, text, media, stats, etc.

// Interactive Components - User interaction components
// Can be deferred until user interaction
import LoadInteractiveComponents from '@universityofmaryland/web-components-library/interactive';
LoadInteractiveComponents(); // Loads: accordion, carousel, footer, social, tab

// Feed Components - Dynamic content feeds
// Load on-demand for pages with feeds
import LoadFeedComponents from '@universityofmaryland/web-components-library/feed';
LoadFeedComponents(); // Loads: events, news, people feeds

Individual Component Exports

Import specific component types individually using dynamic imports for maximum tree-shaking efficiency and granular control over what gets loaded.

// Dynamically import individual component types on demand
const loadSpecificComponents = async () => {
  // Load only card components and their variants
  const cardComponents = await import(
    '@universityofmaryland/web-components-library/components/card'
  );
  cardComponents.standard();  // Register standard card
  cardComponents.overlay();   // Register overlay card
  cardComponents.event();     // Register event card

  // Load only hero components and their variants
  const heroComponents = await import(
    '@universityofmaryland/web-components-library/components/hero'
  );
  heroComponents.base();      // Register base hero
  heroComponents.expand();    // Register expandable hero

  // Load only navigation components
  const navComponents = await import(
    '@universityofmaryland/web-components-library/components/navigation'
  );
  navComponents.primary();    // Register primary navigation
  navComponents.drawer();     // Register navigation drawer
};

// Conditionally load based on DOM presence
if (document.querySelector('[class*="umd-element-card"]')) {
  import('@universityofmaryland/web-components-library/components/card')
    .then(module => module.standard());
}

if (document.querySelector('[class*="umd-element-hero"]')) {
  import('@universityofmaryland/web-components-library/components/hero')
    .then(module => module.base());
}

Bundle Import with Tree-Shaking

Even when using the bundle import, you can still benefit from tree-shaking by importing specific components:

// Import specific components from the bundle for tree-shaking
import { Components, Styles } from '@universityofmaryland/web-components-library/bundle';

// Register only the components you need
Components.card.standard();
Components.hero.base();
Components.navigation.primary();

// The bundler will tree-shake unused components from Elements, Feeds, etc.
// This gives you the convenience of a single import path with optimized output

Export Strategy Comparison

Approach Bundle Size Tree-shaking Development Speed Best For
Grouped Imports Moderate (group-level) Good (per group) Faster setup Standard sites with typical patterns
Individual Imports Smallest possible Maximum efficiency More verbose Production apps with specific needs
Bundle + Tree-shaking Small (with proper imports) Good (when destructured) Simple imports Apps wanting single import path
Full Bundle Init Largest None Quickest Prototypes and development only
💡 Pro Tip: Hybrid Approach

Combine both strategies for optimal results. Use grouped imports for common component sets and individual imports for specific components used sparingly:

// Load structural components as a group
import LoadStructuralComponents from '.../structural';
LoadStructuralComponents();

// Import specific components individually
import { Components } from '@universityofmaryland/web-components-library';
Components.specialFeature.custom(); // Only this specific component

Bundling Configuration

The design system includes an optimized Vite configuration that implements intelligent chunk splitting for optimal caching and loading performance.

Complete Vite Configuration

// vite.config.js
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig(({ mode }) => {
  const isProduction = mode === 'production';

  return {
    build: {
      // Enable/disable source maps based on environment
      sourcemap: !isProduction,

      // Use terser for production minification
      minify: isProduction ? 'terser' : false,

      rollupOptions: {
        input: {
          main: path.resolve(__dirname, 'src/main.ts'),
        },

        output: {
          // Named entry chunks without hash for stable names
          entryFileNames: '[name].js',

          // Smart chunk naming strategy
          chunkFileNames: (chunkInfo) => {
            // Group vendor chunks by library for better caching
            if (chunkInfo.name.includes('styles-library'))
              return 'vendor-styles.js';
            if (chunkInfo.name.includes('elements-library'))
              return 'vendor-elements.js';

            // Default naming with hash for cache busting
            return '[name]-[hash].js';
          },

          // CSS extraction with predictable names
          assetFileNames: (assetInfo) => {
            if (assetInfo.name?.endsWith('.css')) {
              if (assetInfo.name.includes('main')) return 'main.css';
              if (assetInfo.name.includes('template')) return 'template.css';
              return '[name].css';
            }
            return '[name].[ext]';
          },
        },
      },

      // Terser options for aggressive production optimization
      terserOptions: isProduction
        ? {
            compress: {
              drop_console: true,      // Remove console.log in production
              drop_debugger: true,      // Remove debugger statements
            },
          }
        : undefined,

      // Warn about large chunks (500kb)
      chunkSizeWarningLimit: 500,

      // Enable module preload polyfill for older browsers
      modulePreload: {
        polyfill: true,
      },
    },

    resolve: {
      // Path aliases for cleaner imports and grouped components
      alias: {
        '@': path.resolve(__dirname, './source'),

        // Grouped component exports
        '@universityofmaryland/web-components-library/structural':
          path.resolve(__dirname, '../packages/components/dist/structural.js'),
        '@universityofmaryland/web-components-library/content':
          path.resolve(__dirname, '../packages/components/dist/content.js'),
        '@universityofmaryland/web-components-library/interactive':
          path.resolve(__dirname, '../packages/components/dist/interactive.js'),
        '@universityofmaryland/web-components-library/feed':
          path.resolve(__dirname, '../packages/components/dist/feed.js'),

        // Main library exports
        '@universityofmaryland/web-components-library':
          path.resolve(__dirname, '../packages/components/dist/index.js'),
        '@universityofmaryland/web-elements-library':
          path.resolve(__dirname, '../packages/elements'),
        '@universityofmaryland/web-styles-library':
          path.resolve(__dirname, '../packages/styles'),
      },
    },

    optimizeDeps: {
      // Pre-bundle critical dependencies for faster dev server startup
      include: [
        '@universityofmaryland/web-styles-library',
        '@universityofmaryland/web-elements-library',
      ],

      // Scan entry points for dependency discovery
      entries: [
        'src/main.ts',
        'src/styles.ts',
      ],
    },
  };
});
Key Configuration Options Explained:
  • chunkFileNames - Creates predictable vendor chunks for better long-term caching
  • modulePreload - Ensures critical resources are preloaded for faster initial render
  • optimizeDeps.include - Pre-bundles frequently used dependencies to avoid waterfall loading
  • alias - Maps grouped exports to their distribution files for cleaner imports
  • terserOptions - Removes development code in production builds

Dynamic Loading Patterns

The design system implements sophisticated loading strategies to optimize initial page load and progressively enhance the user experience.

Progressive Component Loading

// main.ts - Production-ready loading strategy
import './styles.ts';

// Component loaders with dynamic imports for code splitting
const loadStructuralComponents = async () => {
  const LoadStructuralComponents = (
    await import('@universityofmaryland/web-components-library/structural')
  ).default;
  return LoadStructuralComponents();
};

const loadContentComponents = async () => {
  const LoadContentComponents = (
    await import('@universityofmaryland/web-components-library/content')
  ).default;
  return LoadContentComponents();
};

const loadInteractiveComponents = async () => {
  const LoadInteractiveComponents = (
    await import('@universityofmaryland/web-components-library/interactive')
  ).default;
  return LoadInteractiveComponents();
};

const loadFeedComponents = async () => {
  const LoadFeedComponents = (
    await import('@universityofmaryland/web-components-library/feed')
  ).default;
  return LoadFeedComponents();
};

const loadAnimations = async () => {
  const { Utilties } = await import(
    '@universityofmaryland/web-components-library'
  );
  return Utilties.Animations.loadIntersectionObserver();
};

const initializeApp = async () => {
  // Critical: Load structural components immediately
  await loadStructuralComponents();

  // Visible content: Load when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', async () => {
      await loadContentComponents();
    });
  } else {
    await loadContentComponents();
  }

  // Below-the-fold: Defer interactive components
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      let componentsLoaded = false;

      const loadDeferredComponents = () => {
        if (!componentsLoaded) {
          componentsLoaded = true;
          loadInteractiveComponents();
          loadFeedComponents();
          loadAnimations();
        }
      };

      // Load on any user interaction
      const interactionEvents = [
        'mousedown',
        'touchstart',
        'keydown',
        'scroll',
      ];

      interactionEvents.forEach((event) => {
        document.addEventListener(event, loadDeferredComponents, {
          once: true,
          passive: true,
        });
      });

      // Fallback: Ensure loading after 2 seconds
      setTimeout(loadDeferredComponents, 2000);
    });
  } else {
    // Fallback for browsers without requestIdleCallback
    setTimeout(() => {
      loadInteractiveComponents();
      loadFeedComponents();
      loadAnimations();
    }, 100);
  }
};

initializeApp();

Intersection Observer Pattern

Load component groups only when they're about to enter the viewport. This leverages the pre-built chunks for optimal performance:

// Lazy load component chunks based on viewport visibility
const observeAndLoadComponents = () => {
  // Map components to their optimized chunk groups
  const componentGroups = {
    interactive: ['umd-element-carousel', 'umd-element-accordion', 'umd-element-tab'],
    feed: ['umd-element-feed-events', 'umd-element-feed-news', 'umd-element-feed-people'],
    content: ['umd-element-stats', 'umd-element-card', 'umd-element-text-image'],
  };

  // Track which groups have been loaded
  const loadedGroups = new Set();

  const loadComponentGroup = async (groupName) => {
    if (loadedGroups.has(groupName)) return;
    loadedGroups.add(groupName);

    // Load the optimized chunk for this component group
    const module = await import(
      `@universityofmaryland/web-components-library/${groupName}`
    );
    if (module.default) module.default();
  };

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(async (entry) => {
      if (entry.isIntersecting) {
        const tagName = entry.target.tagName.toLowerCase();

        // Find which group this component belongs to
        for (const [groupName, components] of Object.entries(componentGroups)) {
          if (components.includes(tagName)) {
            await loadComponentGroup(groupName);
            observer.unobserve(entry.target);
            break;
          }
        }
      }
    });
  }, {
    // Start loading when component is 200px away from viewport
    rootMargin: '200px',
  });

  // Observe all uninitialized components
  Object.values(componentGroups).flat().forEach(tagName => {
    document.querySelectorAll(tagName).forEach(element => {
      observer.observe(element);
    });
  });
};

Critical CSS Strategy

The design system implements a two-phase CSS loading strategy to eliminate render-blocking styles and prevent Flash of Unstyled Content (FOUC).

Note: Static file consumption of pre-built CSS files will be available in Release 2.0. The current implementation demonstrates programmatic CSS loading via JavaScript modules.
// styles.ts - Critical CSS loading implementation
import * as Styles from '@universityofmaryland/web-styles-library';

const inlineCriticalStyles = async () => {
  try {
    // Load critical styles in parallel
    const [preRenderCss, fonts] = await Promise.all([
      Styles.preRenderCss,
      Promise.resolve(Styles.typography.fontFace.base64fonts),
    ]);

    // Create inline style element for critical CSS
    const criticalStyle = document.createElement('style');
    criticalStyle.id = 'critical-css';
    criticalStyle.innerHTML = `
      ${fonts}
      ${preRenderCss}
    `;

    // Insert critical styles before any other styles
    const firstStyle = document.head.querySelector('style');
    if (firstStyle) {
      document.head.insertBefore(criticalStyle, firstStyle);
    } else {
      document.head.appendChild(criticalStyle);
    }

    // Load non-critical styles when browser is idle
    requestIdleCallback(() => loadNonCriticalStyles());
  } catch (error) {
    console.error('Failed to load critical styles:', error);
    // Ensure content is visible even if styles fail
    document.body.style.opacity = '1';
  }
};

const loadNonCriticalStyles = async () => {
  try {
    // Load decorative and below-the-fold styles
    const postRenderCss = await Styles.postRenderCss;

    const style = document.createElement('style');
    style.id = 'non-critical-css';
    style.innerHTML = postRenderCss;
    document.head.appendChild(style);
  } catch (error) {
    console.error('Failed to load non-critical styles:', error);
    document.body.style.opacity = '1';
  }
};

// Start loading immediately or when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', inlineCriticalStyles, {
    once: true,
  });
} else {
  inlineCriticalStyles();
}

export { inlineCriticalStyles, loadNonCriticalStyles };
CSS Loading Phases:
  • Phase 1: Critical CSS - Inline base64 fonts and above-the-fold styles directly in the document head
  • Phase 2: Non-Critical CSS - Load decorative styles and animations during idle time
  • Fallback Strategy - Always reveal content if styles fail to load

Performance Metrics

Expected performance improvements when implementing these optimization strategies:

Initial Bundle Size

-65%

Reduction with grouped exports vs full import

Time to Interactive

-40%

Faster with progressive loading strategy

First Contentful Paint

-50%

Improvement with critical CSS inlining

Cache Hit Rate

+80%

Better with vendor chunk splitting

Measuring Performance

// Add performance monitoring to track improvements
const measureComponentLoad = (componentName) => {
  const startMark = `${componentName}-start`;
  const endMark = `${componentName}-end`;
  const measureName = `${componentName}-load`;

  performance.mark(startMark);

  return {
    complete: () => {
      performance.mark(endMark);
      performance.measure(measureName, startMark, endMark);

      const measure = performance.getEntriesByName(measureName)[0];
      console.log(`${componentName} loaded in ${measure.duration.toFixed(2)}ms`);

      // Send to analytics
      if (window.gtag) {
        window.gtag('event', 'timing_complete', {
          name: componentName,
          value: Math.round(measure.duration),
          event_category: 'Component Loading',
        });
      }
    }
  };
};

// Usage example
const heroTimer = measureComponentLoad('hero-components');
await loadStructuralComponents();
heroTimer.complete();

Best Practices Checklist

Follow these recommendations to achieve optimal performance with the UMD Design System:

✅ Loading Strategy

  • Load structural components (hero, navigation) immediately for above-the-fold content
  • Defer interactive components until user interaction or idle time
  • Use Intersection Observer for below-the-fold components
  • Implement fallback timers to ensure components eventually load
  • Leverage requestIdleCallback for non-critical resources

✅ Bundle Optimization

  • Use grouped exports for logical component sets instead of importing everything
  • Configure vendor chunk splitting for better caching across deployments
  • Enable production minification with console.log removal
  • Set appropriate chunk size warning limits (500KB recommended)
  • Use path aliases to simplify imports and improve maintainability

✅ CSS Performance

  • Inline critical CSS including base64 fonts for immediate rendering
  • Defer non-critical styles using requestIdleCallback
  • Extract CSS to separate files for parallel loading
  • Implement smooth transitions to prevent layout shifts
  • Always provide fallbacks for CSS loading failures

✅ Development Workflow

  • Use the bundle analyzer (ANALYZE=true npm run build) to identify optimization opportunities
  • Monitor performance metrics in development to catch regressions early
  • Test with network throttling to simulate real-world conditions
  • Validate that tree-shaking is working correctly for unused components
  • Keep component dependencies minimal to reduce bundle size
🚀 Quick Start Template

Copy this minimal setup for optimal performance out of the box:

// main.js - Minimal performance-optimized setup
import './styles/critical.css';

// Load structural components immediately
import('@universityofmaryland/web-components-library/structural')
  .then(m => m.default());

// Load content when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    import('@universityofmaryland/web-components-library/content')
      .then(m => m.default());
  });
}

// Defer interactive components
requestIdleCallback(() => {
  import('@universityofmaryland/web-components-library/interactive')
    .then(m => m.default());
}, { timeout: 2000 });

Related Documentation