Performance and Accessibility Optimization

1. Lazy Loading Accessible Implementation

Technique Accessibility Consideration Implementation WCAG Impact
Native lazy loading (images) Alt text must be present before loading <img src="..." alt="..." loading="lazy"> No impact if alt text provided (1.1.1)
Intersection Observer Announce loading states with ARIA live regions Load content when visible; update aria-busy during load Ensure loading announced (4.1.3)
Infinite scroll Provide "Load more" button alternative Auto-load + manual button; announce new items loaded User control required (2.2.2, 2.4.1)
Route-based code splitting Manage focus on navigation, announce page change Show loading indicator, focus h1 when loaded Focus order maintained (2.4.3)
Component lazy loading Loading skeleton must have accessible name Use aria-label="Loading..." on placeholder Status messages announced (4.1.3)
Defer off-screen content Ensure keyboard users can reach lazy-loaded content Load before focus reaches; maintain tab order Keyboard accessible (2.1.1)
Image placeholders Low-quality placeholder needs same alt text Alt text on placeholder, maintain on full image Text alternative always present (1.1.1)

Example: Accessible lazy loading patterns

<!-- Native lazy loading with alt text -->
<img 
  src="high-res-image.jpg" 
  alt="Sunset over mountain range with orange and pink sky"
  loading="lazy"
  width="800"
  height="600"
>

<!-- Intersection Observer with loading state -->
<div 
  id="content-section"
  aria-busy="true"
  aria-label="Loading more items..."
>
  <!-- Loading skeleton -->
</div>

<script>
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadContent(entry.target);
    }
  });
}, { rootMargin: '100px' });

async function loadContent(container) {
  container.setAttribute('aria-busy', 'true');
  
  try {
    const content = await fetchContent();
    container.innerHTML = content;
    container.removeAttribute('aria-busy');
    
    // Announce completion
    announce('New content loaded');
  } catch (error) {
    container.setAttribute('aria-busy', 'false');
    announce('Error loading content', 'assertive');
  }
}

observer.observe(document.getElementById('content-section'));
</script>

<!-- Accessible infinite scroll -->
<div role="feed" aria-label="News articles">
  <!-- Articles here -->
</div>

<!-- Manual load alternative -->
<button 
  id="load-more"
  aria-live="polite"
  aria-atomic="true"
>
  Load more articles
</button>

<script>
let autoLoadEnabled = true;

// Auto-load on scroll
window.addEventListener('scroll', () => {
  if (!autoLoadEnabled) return;
  
  const scrollable = document.documentElement.scrollHeight - window.innerHeight;
  const scrolled = window.scrollY;
  
  if (scrolled / scrollable > 0.8) {
    loadMoreArticles();
  }
});

// Manual load button
document.getElementById('load-more').addEventListener('click', () => {
  autoLoadEnabled = false;
  loadMoreArticles();
});

async function loadMoreArticles() {
  const button = document.getElementById('load-more');
  button.textContent = 'Loading...';
  button.disabled = true;
  
  const newArticles = await fetchArticles();
  appendArticles(newArticles);
  
  // Announce to screen readers
  button.textContent = `${newArticles.length} new articles loaded. Load more`;
  button.disabled = false;
  
  // Alternative: use live region
  announce(`${newArticles.length} new articles added to feed`);
}
</script>

<!-- React lazy loading with Suspense -->
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={
      <div aria-live="polite" aria-busy="true">
        <span className="sr-only">Loading component...</span>
        <div className="spinner" aria-hidden="true"></div>
      </div>
    }>
      <HeavyComponent />
    </Suspense>
  );
}
Lazy Loading Pitfalls: Don't lazy load critical content above the fold. Ensure alt text present before image loads. Announce loading states for dynamic content. Provide manual alternatives for infinite scroll. Maintain focus position when lazy loading near focused element.

2. Progressive Enhancement Strategies

Layer Base Experience Enhanced Experience Accessibility Benefit
HTML (Structure) Semantic HTML, native elements Custom components, ARIA enhancements Works without JS; screen reader compatible
CSS (Presentation) Readable without CSS, logical source order Enhanced layouts, animations, theming Content accessible if CSS fails to load
JavaScript (Behavior) Forms submit to server, links navigate Client-side validation, SPA navigation, AJAX Core functionality works without JS
Images Alt text, semantic HTML Responsive images, lazy loading, WebP Alt text ensures content accessible
Forms Native HTML5 validation, server-side validation Enhanced client-side validation, auto-save Works with JS disabled; native validation accessible
Navigation Standard links, skip links SPA routing, smooth scroll, prefetching Links work universally; progressive for performance
Media Captions, transcripts in HTML Custom players, adaptive streaming Captions always available; enhanced player optional

Example: Progressive enhancement patterns

<!-- BASE: Works without JavaScript -->
<form action="/search" method="GET">
  <label for="search-input">Search</label>
  <input 
    type="search" 
    id="search-input" 
    name="q" 
    required
    minlength="2"
  >
  <button type="submit">Search</button>
</form>

<!-- ENHANCED: JavaScript adds autocomplete -->
<script>
// Feature detection
if ('IntersectionObserver' in window) {
  const searchInput = document.getElementById('search-input');
  const form = searchInput.closest('form');
  
  // Enhance with autocomplete
  searchInput.setAttribute('role', 'combobox');
  searchInput.setAttribute('aria-autocomplete', 'list');
  searchInput.setAttribute('aria-expanded', 'false');
  
  // Create autocomplete list
  const listbox = document.createElement('ul');
  listbox.setAttribute('role', 'listbox');
  listbox.id = 'autocomplete-list';
  searchInput.setAttribute('aria-controls', 'autocomplete-list');
  form.appendChild(listbox);
  
  // Add autocomplete functionality
  searchInput.addEventListener('input', async (e) => {
    const results = await fetchSuggestions(e.target.value);
    displaySuggestions(results);
  });
  
  // Form still submits normally if autocomplete fails
}
</script>

<!-- BASE: Native disclosure -->
<details>
  <summary>Show more information</summary>
  <p>Additional content here...</p>
</details>

<!-- ENHANCED: Custom accordion with animation -->
<script>
// Enhance details element if animations supported
if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
  const details = document.querySelector('details');
  const content = details.querySelector('p');
  
  // Add smooth animation
  details.addEventListener('toggle', () => {
    if (details.open) {
      content.style.animation = 'slideDown 0.3s ease-out';
    }
  });
}
</script>

<!-- Progressive image loading -->
<picture>
  <!-- Base: JPEG for all browsers -->
  <source srcset="image.webp" type="image/webp">
  <img 
    src="image.jpg" 
    alt="Product photo showing red sneakers"
    loading="lazy"
    width="600"
    height="400"
  >
</picture>

<!-- CSS Progressive Enhancement -->
<style>
/* Base: Works everywhere */
.button {
  padding: 12px 24px;
  background: #0066cc;
  color: white;
  border: 2px solid #0066cc;
}

/* Enhanced: Modern browsers */
@supports (display: grid) {
  .layout {
    display: grid;
    gap: 20px;
  }
}

/* Enhanced: Interaction */
@media (hover: hover) {
  .button:hover {
    background: #0052a3;
  }
}

/* Enhanced: Animations for users who allow them */
@media (prefers-reduced-motion: no-preference) {
  .button {
    transition: background 0.2s ease;
  }
}
</style>

<!-- JavaScript feature detection -->
<script>
// Check for feature support before enhancing
class ProgressiveEnhancer {
  static enhance() {
    // Check required features
    if (!this.supportsFeatures()) {
      console.log('Using base experience');
      return;
    }
    
    // Add enhancements
    this.enhanceForms();
    this.enhanceNavigation();
  }
  
  static supportsFeatures() {
    return (
      'IntersectionObserver' in window &&
      'requestIdleCallback' in window &&
      'fetch' in window
    );
  }
  
  static enhanceForms() {
    // Add client-side validation
    // Add auto-save
    // Add inline feedback
  }
  
  static enhanceNavigation() {
    // Add SPA routing
    // Add prefetching
    // Add smooth scroll
  }
}

// Only enhance if supported
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    ProgressiveEnhancer.enhance();
  });
} else {
  ProgressiveEnhancer.enhance();
}
</script>
Progressive Enhancement Benefits: Core content/functionality always accessible. Graceful degradation for older browsers/assistive tech. Better performance (load only what's needed). More resilient to JavaScript errors. Improved SEO and initial render.

3. Critical Path and Assistive Technologies

Optimization AT Impact Implementation Performance Benefit
Critical CSS inline Ensures focus indicators visible immediately Inline styles for above-fold, focus states, ARIA Faster First Contentful Paint (FCP)
Defer non-critical JS Core HTML accessible before JS loads Use defer/async; load enhancements after interactive Faster Time to Interactive (TTI)
Preload fonts Prevent text reflow that breaks screen reader context <link rel="preload" href="font.woff2" as="font"> Reduce Cumulative Layout Shift (CLS)
Resource hints Faster navigation for keyboard/AT users dns-prefetch, preconnect for critical domains Reduce navigation latency
Priority hints Ensure alt text images load first <img fetchpriority="high"> for hero images Optimize Largest Contentful Paint (LCP)
Service worker caching Offline access for all users including AT users Cache critical HTML, CSS, and accessibility features Instant repeat visits
Reduce DOM size Faster screen reader navigation and parsing Flatten nesting, remove unnecessary divs Lower memory usage, faster rendering

Example: Critical path optimization for accessibility

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Page Title</title>
  
  <!-- Critical CSS inline (includes focus styles, basic layout) -->
  <style>
    /* Critical styles for accessibility */
    *:focus-visible {
      outline: 2px solid #0066cc;
      outline-offset: 2px;
    }
    
    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border-width: 0;
    }
    
    /* Critical layout */
    body {
      margin: 0;
      font-family: system-ui, sans-serif;
      line-height: 1.5;
    }
    
    main {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }
  </style>
  
  <!-- Preload critical font (prevents layout shift) -->
  <link 
    rel="preload" 
    href="/fonts/main-font.woff2" 
    as="font" 
    type="font/woff2" 
    crossorigin
  >
  
  <!-- Preconnect to critical domains -->
  <link rel="preconnect" href="https://api.example.com">
  <link rel="dns-prefetch" href="https://cdn.example.com">
  
  <!-- Defer non-critical CSS -->
  <link 
    rel="stylesheet" 
    href="/css/styles.css" 
    media="print" 
    onload="this.media='all'"
  >
</head>
<body>
  <!-- Skip link (critical for keyboard users) -->
  <a href="#main-content" class="sr-only">Skip to main content</a>
  
  <header>
    <h1>Site Title</h1>
    <nav aria-label="Main navigation">
      <!-- Navigation -->
    </nav>
  </header>
  
  <main id="main-content">
    <!-- Critical above-fold content -->
    <h2>Page Heading</h2>
    
    <!-- Hero image with priority -->
    <img 
      src="hero.jpg" 
      alt="Description of hero image"
      fetchpriority="high"
      width="1200"
      height="600"
    >
  </main>
  
  <!-- Defer non-critical JavaScript -->
  <script defer src="/js/main.js"></script>
  
  <!-- Load analytics asynchronously -->
  <script async src="/js/analytics.js"></script>
</body>
</html>

<!-- Service Worker for offline accessibility -->
<script>
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(registration => {
    console.log('Service Worker registered');
  });
}
</script>

<!-- sw.js (Service Worker) -->
const CACHE_VERSION = 'v1';
const CRITICAL_CACHE = [
  '/',
  '/css/critical.css',
  '/js/accessibility.js',
  '/fonts/main-font.woff2'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then(cache => {
      return cache.addAll(CRITICAL_CACHE);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});
</script>
Critical Path Warnings: Don't defer CSS with focus styles - inline them. Ensure skip links work before JavaScript loads. Test with slow 3G connections. Screen readers may struggle with large DOM trees (>1500 nodes). Always measure real-world AT performance.

4. Bundle Optimization for A11y Libraries

Library/Technique Size Impact Optimization Strategy Trade-offs
focus-trap-react ~3KB gzipped Use only for complex modals; native dialog element when possible Native dialog has better browser integration but less control
react-aria ~50-100KB (tree-shakeable) Import only needed hooks; use modular imports Comprehensive but large; consider lighter alternatives
@reach/ui ~30KB per component Import individual components, not full package Accessible but adds bundle size
axe-core (dev only) ~500KB (dev dependency) Strip from production builds; use only in tests Critical for testing but must not ship to prod
Custom ARIA utilities 1-5KB custom code Write lightweight utilities for common patterns More maintenance but smaller bundle
Polyfills (inert, dialog) ~10-20KB Load conditionally based on feature detection Only users with older browsers pay the cost
ARIA live region library ~2KB custom Build minimal announcer utility instead of library Custom code needs testing but very light

Example: Bundle optimization strategies

// BAD: Import entire library
import * as ReachUI from '@reach/ui';

// GOOD: Import only what you need
import { Dialog } from '@reach/dialog';
import '@reach/dialog/styles.css';

// BETTER: Use native when possible
// <dialog> element has built-in focus trap and accessibility

// Tree-shaking react-aria
// BAD: Imports everything
import { useButton, useDialog, useFocusRing } from 'react-aria';

// GOOD: Modular imports (better tree-shaking)
import { useButton } from '@react-aria/button';
import { useDialog } from '@react-aria/dialog';
import { useFocusRing } from '@react-aria/focus';

// Conditional polyfill loading
async function loadPolyfillsIfNeeded() {
  const polyfills = [];
  
  // Inert polyfill (for focus trapping)
  if (!('inert' in HTMLElement.prototype)) {
    polyfills.push(import('wicg-inert'));
  }
  
  // Dialog polyfill
  if (!window.HTMLDialogElement) {
    polyfills.push(import('dialog-polyfill'));
  }
  
  await Promise.all(polyfills);
}

// Lightweight custom announcer (vs importing library)
class Announcer {
  constructor() {
    this.region = this.createRegion();
  }
  
  createRegion() {
    const div = document.createElement('div');
    div.className = 'sr-only';
    div.setAttribute('aria-live', 'polite');
    div.setAttribute('aria-atomic', 'true');
    document.body.appendChild(div);
    return div;
  }
  
  announce(message) {
    this.region.textContent = '';
    setTimeout(() => {
      this.region.textContent = message;
    }, 100);
  }
}

// Size: ~0.5KB vs ~5KB for a library

// Webpack configuration for optimization
module.exports = {
  optimization: {
    usedExports: true, // Tree shaking
    sideEffects: false,
    
    splitChunks: {
      cacheGroups: {
        // Separate a11y libraries for caching
        accessibility: {
          test: /[\\/]node_modules[\\/](@react-aria|@reach)/,
          name: 'a11y-vendors',
          chunks: 'all',
        },
      },
    },
  },
  
  // Don't include axe-core in production
  externals: process.env.NODE_ENV === 'production' ? {
    'axe-core': 'axe-core',
    '@axe-core/react': '@axe-core/react'
  } : {},
};

// Vite configuration
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'a11y': [
            '@react-aria/button',
            '@react-aria/dialog',
            'focus-trap-react'
          ]
        }
      }
    }
  }
};

// Measure bundle impact
// Run: npx webpack-bundle-analyzer dist/stats.json

// Example package.json for production builds
{
  "scripts": {
    "build": "NODE_ENV=production webpack --mode production",
    "analyze": "webpack-bundle-analyzer dist/stats.json"
  },
  "dependencies": {
    "@reach/dialog": "^0.18.0"
  },
  "devDependencies": {
    "axe-core": "^4.7.0",
    "@axe-core/react": "^4.7.0",
    "webpack-bundle-analyzer": "^4.9.0"
  }
}
Bundle Optimization Tips: Use native browser features when available (dialog, details, inert). Import only specific functions, not entire libraries. Strip dev-only accessibility tools (axe-core) from production. Use dynamic imports for large a11y features. Measure with webpack-bundle-analyzer.

5. Runtime Performance Considerations

Performance Issue AT Impact Solution Measurement
Heavy ARIA DOM updates Screen readers lag or miss announcements Debounce updates, batch DOM mutations, use requestIdleCallback Monitor main thread blocking time
Large accessibility tree Slow screen reader navigation (>1500 nodes) Virtualize long lists, lazy load off-screen content Count accessibility tree nodes in DevTools
Live region spam Excessive announcements overwhelm users Throttle updates, combine multiple changes into one announcement Test with screen reader enabled
Focus management overhead Janky focus transitions Use CSS containment, minimize reflows on focus Lighthouse Performance score, TBT metric
ARIA attribute thrashing Screen reader interruptions Update ARIA atomically, avoid rapid toggles Screen reader testing
Memory leaks (event listeners) Degraded AT performance over time Clean up event listeners, use AbortController Chrome DevTools Memory profiler
Layout thrashing Slow response to keyboard navigation Batch reads and writes, use CSS transforms for position Performance DevTools, measure frame rate

Example: Runtime performance optimization

// Debounce live region updates
class ThrottledAnnouncer {
  constructor(delay = 1000) {
    this.delay = delay;
    this.timer = null;
    this.region = this.createRegion();
  }
  
  createRegion() {
    const div = document.createElement('div');
    div.className = 'sr-only';
    div.setAttribute('aria-live', 'polite');
    div.setAttribute('aria-atomic', 'true');
    document.body.appendChild(div);
    return div;
  }
  
  announce(message) {
    clearTimeout(this.timer);
    
    this.timer = setTimeout(() => {
      this.region.textContent = message;
    }, this.delay);
  }
}

// Usage: Prevents announcement spam
const announcer = new ThrottledAnnouncer(1000);

// Instead of announcing every character typed
input.addEventListener('input', (e) => {
  announcer.announce(`${e.target.value.length} characters entered`);
});

// Batch DOM updates for accessibility tree
function updateAccessibilityTree(items) {
  // BAD: Multiple forced reflows
  items.forEach(item => {
    item.setAttribute('aria-selected', 'false');
    item.setAttribute('aria-expanded', 'false');
  });
  
  // GOOD: Batch updates
  requestAnimationFrame(() => {
    items.forEach(item => {
      item.setAttribute('aria-selected', 'false');
      item.setAttribute('aria-expanded', 'false');
    });
  });
}

// Virtual scrolling for large lists (a11y-friendly)
class AccessibleVirtualList {
  constructor(container, items, rowHeight = 50) {
    this.container = container;
    this.items = items;
    this.rowHeight = rowHeight;
    this.visibleCount = Math.ceil(container.clientHeight / rowHeight);
    
    this.render();
  }
  
  render() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.floor(scrollTop / this.rowHeight);
    const endIndex = startIndex + this.visibleCount;
    
    // Render only visible items
    const visibleItems = this.items.slice(startIndex, endIndex);
    
    // Maintain accessibility tree size
    this.container.innerHTML = visibleItems.map((item, index) => `
      <div 
        role="listitem" 
        aria-setsize="${this.items.length}"
        aria-posinset="${startIndex + index + 1}"
      >
        ${item.content}
      </div>
    `).join('');
    
    // Set container ARIA attributes
    this.container.setAttribute('role', 'list');
    this.container.setAttribute('aria-label', 
      `List with ${this.items.length} items`
    );
  }
}

// Use requestIdleCallback for non-critical updates
function enhanceAccessibility() {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      // Add ARIA enhancements during idle time
      addAriaLabels();
      addKeyboardHandlers();
    }, { timeout: 2000 });
  } else {
    // Fallback
    setTimeout(() => {
      addAriaLabels();
      addKeyboardHandlers();
    }, 100);
  }
}

// Clean up event listeners to prevent memory leaks
class AccessibleComponent {
  constructor(element) {
    this.element = element;
    this.controller = new AbortController();
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    // All listeners use same AbortController
    this.element.addEventListener('click', this.handleClick, {
      signal: this.controller.signal
    });
    
    this.element.addEventListener('keydown', this.handleKeydown, {
      signal: this.controller.signal
    });
  }
  
  handleClick = () => { /* ... */ };
  handleKeydown = () => { /* ... */ };
  
  destroy() {
    // Remove all listeners at once
    this.controller.abort();
  }
}

// Monitor accessibility tree size
function checkAccessibilityTreeSize() {
  const allElements = document.querySelectorAll('*');
  const interactiveElements = document.querySelectorAll(
    'button, a, input, select, textarea, [tabindex], [role]'
  );
  
  console.log('Total DOM nodes:', allElements.length);
  console.log('Interactive/ARIA nodes:', interactiveElements.length);
  
  if (interactiveElements.length > 1500) {
    console.warn('Large accessibility tree - consider virtualization');
  }
}

// Performance measurement
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'measure') {
      console.log(`${entry.name}: ${entry.duration}ms`);
    }
  }
});

observer.observe({ entryTypes: ['measure'] });

performance.mark('aria-update-start');
// ... ARIA updates ...
performance.mark('aria-update-end');
performance.measure('ARIA Update Time', 'aria-update-start', 'aria-update-end');
Performance Pitfalls: Excessive ARIA updates cause screen reader lag. Large accessibility trees (>1500 nodes) slow down AT navigation. Rapid live region changes overwhelm users. Always test performance with screen readers enabled. Use Chrome DevTools Accessibility Tree to count nodes.

Performance and Accessibility Quick Reference

  • Lazy Loading: Use loading="lazy" with alt text; announce loading states with aria-live; provide "Load more" alternative for infinite scroll
  • Progressive Enhancement: Start with semantic HTML; enhance with CSS/JS; ensure core functionality works without JavaScript
  • Critical Path: Inline critical CSS including focus styles; defer non-critical JS; preload fonts to prevent reflow
  • Bundle Optimization: Tree-shake a11y libraries; use native features (dialog, details); strip axe-core from production
  • Runtime Performance: Debounce live region updates; virtualize long lists; keep accessibility tree <1500 nodes; batch DOM updates
  • Measurements: Lighthouse accessibility score; count accessibility tree nodes; monitor Total Blocking Time (TBT); test with AT enabled
  • Service Workers: Cache critical accessibility features for offline access; ensure skip links work without JS
  • Memory: Clean up event listeners with AbortController; avoid ARIA attribute thrashing; prevent memory leaks in SPAs
  • Best Practices: Test performance with screen readers enabled; measure real-world AT performance; optimize for slow connections
  • Tools: Lighthouse CI, webpack-bundle-analyzer, Chrome DevTools Performance, Accessibility Tree inspector