Browser Compatibility Strategies

Writing Code That Works Everywhere

The Universal Remote Control

Imagine creating a universal remote control that needs to work with TVs from the 1990s all the way to the latest smart TVs. Each TV has different capabilities - older ones might only respond to basic commands, while newer ones support voice control and internet connectivity. This is exactly the challenge we face with browser compatibility!

graph TD A[Browser Compatibility] --> B[Detection] A -> C[Polyfills] A -> D[Progressive Enhancement] A -> E[Graceful Degradation] A -> F[Feature Detection] A -> G[Cross-Browser Testing] B --> B1[User Agent] B --> B2[Feature Support] B --> B3[CSS Support] C --> C1[Runtime Polyfills] C --> C2[Build-time Polyfills] C --> C3[Conditional Loading] D --> D1[Core Functionality] D --> D2[Enhanced Features] D --> D3[Cutting-edge Features] E --> E1[Full Experience] E --> E2[Reduced Experience] E --> E3[Basic Experience] F --> F1[Modernizr] F --> F2[Native Detection] F --> F3[CSS @supports] G --> G1[BrowserStack] G --> G2[Manual Testing] G --> G3[Automated Testing] style A fill:#f9f,stroke:#333,stroke-width:4px

Understanding Browser Differences

Before we can create compatible code, we need to understand what makes browsers different:

1. JavaScript Engine Differences

Browser JavaScript Engine Key Characteristics
Chrome V8 Fast execution, good ES6+ support
Firefox SpiderMonkey Good standards compliance
Safari JavaScriptCore Sometimes slower to adopt features
Edge (Chromium) V8 Similar to Chrome
Internet Explorer 11 Chakra Limited ES6 support

2. CSS Support Variations

/* Modern CSS with fallbacks */
.container {
  /* Fallback for older browsers */
  display: block;
  
  /* Modern browsers */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
}

/* Feature detection with @supports */
@supports (display: grid) {
  .container {
    display: grid;
  }
}

/* Vendor prefixes for older browsers */
.animation {
  -webkit-animation: slide 1s ease;
  -moz-animation: slide 1s ease;
  animation: slide 1s ease;
}

3. API Availability

// Check for API availability before using
if ('IntersectionObserver' in window) {
  // Use IntersectionObserver
  const observer = new IntersectionObserver(callback);
} else {
  // Fall back to scroll events
  window.addEventListener('scroll', fallbackFunction);
}

// Fetch API with fallback
if (window.fetch) {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data));
} else {
  // Fallback to XMLHttpRequest
  const xhr = new XMLHttpRequest();
  xhr.open('GET', '/api/data');
  xhr.onload = () => console.log(JSON.parse(xhr.responseText));
  xhr.send();
}

Feature Detection Strategies

Feature detection is the cornerstone of browser compatibility. Instead of checking which browser is being used, we check if specific features are available:

1. Native Feature Detection

// JavaScript feature detection
const supportsPromises = typeof Promise !== 'undefined';
const supportsArrowFunctions = (() => {
  try {
    eval('() => {}');
    return true;
  } catch (e) {
    return false;
  }
})();

// DOM API detection
const supportsFetch = 'fetch' in window;
const supportsWebGL = (() => {
  try {
    const canvas = document.createElement('canvas');
    return !!(window.WebGLRenderingContext && 
      (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
  } catch(e) {
    return false;
  }
})();

// CSS feature detection
const supportsGrid = CSS.supports('display', 'grid');
const supportsCustomProperties = CSS.supports('--custom', 'value');

2. Using Modernizr

// Install Modernizr
npm install modernizr

// Create a custom build with only needed tests
// modernizr-config.json
{
  "minify": true,
  "options": [
    "setClasses",
    "addTest",
    "html5printshiv",
    "testProp",
    "fnBind"
  ],
  "feature-detects": [
    "css/flexbox",
    "css/grid",
    "es6/promises",
    "serviceworker"
  ]
}

// Using Modernizr in code
if (Modernizr.flexbox) {
  // Use flexbox
  element.style.display = 'flex';
} else {
  // Use fallback layout
  element.style.display = 'table';
}

// CSS classes automatically added to HTML
/* In your CSS */
.flexbox .container {
  display: flex;
}

.no-flexbox .container {
  display: table;
}

3. Custom Feature Detection

// Create a feature detection utility
const features = {
  // Check for Passive Event Listeners
  passiveEvents: (() => {
    let supportsPassive = false;
    try {
      const opts = Object.defineProperty({}, 'passive', {
        get: function() {
          supportsPassive = true;
          return true;
        }
      });
      window.addEventListener('testPassive', null, opts);
      window.removeEventListener('testPassive', null, opts);
    } catch (e) {}
    return supportsPassive;
  })(),
  
  // Check for Service Worker
  serviceWorker: 'serviceWorker' in navigator,
  
  // Check for Touch Events
  touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
  
  // Check for Local Storage
  localStorage: (() => {
    try {
      localStorage.setItem('test', 'test');
      localStorage.removeItem('test');
      return true;
    } catch(e) {
      return false;
    }
  })()
};

// Use the feature detection
if (features.passiveEvents) {
  element.addEventListener('scroll', handler, { passive: true });
} else {
  element.addEventListener('scroll', handler);
}

Polyfill Strategies

Polyfills add missing functionality to older browsers. Let's explore different approaches:

flowchart TD A[Polyfill Strategy] --> B{Feature Detection} B -->|Feature Missing| C[Load Polyfill] B -->|Feature Exists| D[Use Native Implementation] C --> E[Runtime Loading] C --> F[Build-time Inclusion] C --> G[Conditional Loading] E --> H[Dynamic Import] E --> I[Script Injection] F --> J[Webpack Bundle] F --> K[Babel Polyfills] G --> L[Polyfill.io] G --> M[Feature-based Loading] style B fill:#f9f,stroke:#333 style C fill:#ff9,stroke:#333 style D fill:#9f9,stroke:#333

1. Manual Polyfill Implementation

// Array.prototype.includes polyfill
if (!Array.prototype.includes) {
  Array.prototype.includes = function(searchElement, fromIndex) {
    if (this == null) {
      throw new TypeError('"this" is null or not defined');
    }
    
    const o = Object(this);
    const len = o.length >>> 0;
    
    if (len === 0) {
      return false;
    }
    
    const n = fromIndex | 0;
    let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
    
    while (k < len) {
      if (o[k] === searchElement) {
        return true;
      }
      k++;
    }
    
    return false;
  };
}

// Promise polyfill (simplified)
if (typeof Promise === 'undefined') {
  window.Promise = function(executor) {
    let onResolve, onReject;
    let fulfilled = false;
    let rejected = false;
    let value;
    
    this.then = function(onFulfilled, onRejected) {
      if (fulfilled) {
        onFulfilled(value);
      } else {
        onResolve = onFulfilled;
      }
      
      if (rejected) {
        onRejected(value);
      } else {
        onReject = onRejected;
      }
      
      return this;
    };
    
    function resolve(val) {
      value = val;
      fulfilled = true;
      if (onResolve) {
        onResolve(value);
      }
    }
    
    function reject(reason) {
      value = reason;
      rejected = true;
      if (onReject) {
        onReject(value);
      }
    }
    
    executor(resolve, reject);
  };
}

2. Using Polyfill.io








3. Dynamic Polyfill Loading

// Feature-based dynamic loading
async function loadPolyfills() {
  const polyfills = [];
  
  if (!window.Promise) {
    polyfills.push(import('promise-polyfill'));
  }
  
  if (!window.fetch) {
    polyfills.push(import('whatwg-fetch'));
  }
  
  if (!Element.prototype.closest) {
    polyfills.push(import('element-closest-polyfill'));
  }
  
  if (polyfills.length > 0) {
    await Promise.all(polyfills);
  }
}

// Load polyfills before app initialization
loadPolyfills().then(() => {
  // Initialize your application
  initApp();
});

// Using dynamic import with feature detection
if (!window.IntersectionObserver) {
  import('intersection-observer').then(() => {
    // Now we can use IntersectionObserver
    setupLazyLoading();
  });
} else {
  setupLazyLoading();
}

Progressive Enhancement vs Graceful Degradation

Two philosophical approaches to browser compatibility:

graph LR A[Progressive Enhancement] --> B[Start with basic HTML] B --> C[Add CSS for presentation] C --> D[Enhance with JavaScript] D --> E[Add advanced features] F[Graceful Degradation] --> G[Build full experience] G --> H[Test in older browsers] H --> I[Add fallbacks] I --> J[Ensure basic functionality] style A fill:#9f9,stroke:#333 style F fill:#f99,stroke:#333

Progressive Enhancement Example


Graceful Degradation Example

// Start with full functionality
class ImageGallery {
  constructor(element) {
    this.element = element;
    this.images = Array.from(element.querySelectorAll('img'));
    
    // Check for required features
    if (this.checkSupport()) {
      this.initAdvancedGallery();
    } else {
      this.initBasicGallery();
    }
  }
  
  checkSupport() {
    return 'IntersectionObserver' in window &&
           'fetch' in window &&
           CSS.supports('display', 'grid');
  }
  
  initAdvancedGallery() {
    // Full experience with lazy loading and animations
    this.setupLazyLoading();
    this.setupAnimations();
    this.setupTouchGestures();
  }
  
  initBasicGallery() {
    // Degraded experience for older browsers
    this.setupBasicNavigation();
    this.loadAllImages();
  }
  
  setupLazyLoading() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.loadImage(entry.target);
        }
      });
    });
    
    this.images.forEach(img => observer.observe(img));
  }
  
  setupBasicNavigation() {
    // Simple previous/next buttons
    const prevButton = document.createElement('button');
    const nextButton = document.createElement('button');
    
    prevButton.textContent = 'Previous';
    nextButton.textContent = 'Next';
    
    // Add basic click handlers
    prevButton.onclick = () => this.navigate(-1);
    nextButton.onclick = () => this.navigate(1);
  }
}

Cross-Browser Testing Strategies

Testing across browsers is crucial for ensuring compatibility:

1. Manual Testing Checklist

// Browser Testing Checklist
const testingChecklist = {
  browsers: [
    'Chrome (latest)',
    'Firefox (latest)',
    'Safari (latest)',
    'Edge (latest)',
    'Internet Explorer 11',
    'Mobile Safari (iOS)',
    'Chrome (Android)'
  ],
  
  features: [
    'Page layout',
    'Form submission',
    'JavaScript functionality',
    'CSS animations',
    'Responsive design',
    'Touch interactions',
    'Keyboard navigation',
    'Screen reader compatibility'
  ],
  
  scenarios: [
    'Fast connection',
    'Slow connection',
    'JavaScript disabled',
    'CSS disabled',
    'Different screen sizes',
    'Different input methods'
  ]
};

2. Automated Testing

// Using Selenium WebDriver for cross-browser testing
const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const firefox = require('selenium-webdriver/firefox');

async function runCrossBrowserTest() {
  const browsers = [
    { name: 'chrome', builder: new Builder().forBrowser('chrome') },
    { name: 'firefox', builder: new Builder().forBrowser('firefox') }
  ];
  
  for (const browser of browsers) {
    const driver = await browser.builder.build();
    
    try {
      await driver.get('http://localhost:3000');
      
      // Test page load
      await driver.wait(until.titleIs('My App'), 5000);
      
      // Test form submission
      await driver.findElement(By.id('username')).sendKeys('testuser');
      await driver.findElement(By.id('password')).sendKeys('password');
      await driver.findElement(By.css('button[type="submit"]')).click();
      
      // Verify navigation
      await driver.wait(until.urlContains('/dashboard'), 5000);
      
      console.log(`✓ Tests passed on ${browser.name}`);
    } catch (error) {
      console.error(`✗ Tests failed on ${browser.name}:`, error);
    } finally {
      await driver.quit();
    }
  }
}

// Using Playwright for modern cross-browser testing
const { chromium, firefox, webkit } = require('playwright');

async function testWithPlaywright() {
  const browsers = [chromium, firefox, webkit];
  
  for (const browserType of browsers) {
    const browser = await browserType.launch();
    const context = await browser.newContext();
    const page = await context.newPage();
    
    await page.goto('http://localhost:3000');
    
    // Test interactions
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
    
    // Verify results
    await page.waitForSelector('.dashboard');
    
    await browser.close();
  }
}

3. Browser Testing Services

// BrowserStack configuration
// browserstack.json
{
  "auth": {
    "username": "YOUR_USERNAME",
    "access_key": "YOUR_ACCESS_KEY"
  },
  "browsers": [
    {
      "browser": "chrome",
      "browser_version": "latest",
      "os": "Windows",
      "os_version": "10"
    },
    {
      "browser": "safari",
      "browser_version": "14.0",
      "os": "OS X",
      "os_version": "Big Sur"
    },
    {
      "browser": "firefox",
      "browser_version": "latest",
      "os": "Windows",
      "os_version": "10"
    }
  ],
  "run_settings": {
    "cypress_config_file": "./cypress.json",
    "project_name": "My Web App",
    "build_name": "Cross-browser Testing",
    "parallels": 5
  }
}

Handling CSS Compatibility

CSS compatibility requires special attention:

1. Feature Queries

/* Base styles for all browsers */
.container {
  display: block;
  margin: 0 auto;
  max-width: 1200px;
}

/* Progressive enhancement with feature queries */
@supports (display: grid) {
  .container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
  }
}

/* Fallback for flexbox if grid not supported */
@supports (display: flex) and (not (display: grid)) {
  .container {
    display: flex;
    flex-wrap: wrap;
  }
  
  .container > * {
    flex: 1 1 300px;
    margin: 10px;
  }
}

/* Multiple feature detection */
@supports (display: grid) and (gap: 20px) {
  .advanced-grid {
    display: grid;
    gap: 20px;
  }
}

/* Nested feature queries */
@supports (display: flex) {
  .flex-container {
    display: flex;
    
    @supports (gap: 1rem) {
      gap: 1rem;
    }
    
    @supports not (gap: 1rem) {
      margin: -0.5rem;
      
      > * {
        margin: 0.5rem;
      }
    }
  }
}

2. CSS Variables with Fallbacks

/* CSS Custom Properties with fallbacks */
:root {
  --primary-color: #007bff;
  --secondary-color: #6c757d;
  --spacing-unit: 1rem;
}

.button {
  /* Fallback for browsers without CSS variables */
  background-color: #007bff;
  background-color: var(--primary-color);
  
  padding: 0.5rem 1rem;
  padding: calc(var(--spacing-unit) * 0.5) var(--spacing-unit);
}

/* Feature detection for CSS variables */
@supports (--css: variables) {
  .enhanced-component {
    --local-spacing: calc(var(--spacing-unit) * 2);
    margin: var(--local-spacing);
  }
}

/* JavaScript fallback for CSS variables */
if (!CSS.supports('--css', 'variables')) {
  // Polyfill CSS variables or provide fallbacks
  document.documentElement.style.setProperty('--primary-color', '#007bff');
}

3. Vendor Prefixes and Autoprefixer

/* Manual vendor prefixes (old approach) */
.animated {
  -webkit-animation: slide 1s ease;
  -moz-animation: slide 1s ease;
  -ms-animation: slide 1s ease;
  -o-animation: slide 1s ease;
  animation: slide 1s ease;
}

/* Using Autoprefixer (recommended) */
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead']
    })
  ]
}

// Input CSS
.example {
  display: flex;
  transition: all 0.3s;
  user-select: none;
}

// Output CSS (after Autoprefixer)
.example {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-transition: all 0.3s;
  transition: all 0.3s;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

Real-World Compatibility Patterns

Let's look at common patterns for handling compatibility in production applications:

1. Feature Detection Service

// services/featureDetection.js
class FeatureDetectionService {
  constructor() {
    this.features = new Map();
    this.runDetection();
  }
  
  runDetection() {
    // JavaScript features
    this.detect('promises', () => typeof Promise !== 'undefined');
    this.detect('fetch', () => 'fetch' in window);
    this.detect('async', () => {
      try {
        eval('async () => {}');
        return true;
      } catch (e) {
        return false;
      }
    });
    
    // DOM features
    this.detect('intersectionObserver', () => 'IntersectionObserver' in window);
    this.detect('mutationObserver', () => 'MutationObserver' in window);
    this.detect('customElements', () => 'customElements' in window);
    
    // CSS features
    this.detect('grid', () => CSS.supports('display', 'grid'));
    this.detect('sticky', () => CSS.supports('position', 'sticky'));
    this.detect('customProperties', () => CSS.supports('--test', '0'));
    
    // Device features
    this.detect('touch', () => 'ontouchstart' in window);
    this.detect('geolocation', () => 'geolocation' in navigator);
    this.detect('serviceWorker', () => 'serviceWorker' in navigator);
  }
  
  detect(feature, test) {
    try {
      this.features.set(feature, test());
    } catch (e) {
      this.features.set(feature, false);
    }
  }
  
  supports(feature) {
    return this.features.get(feature) || false;
  }
  
  async loadPolyfillsFor(features) {
    const needed = features.filter(f => !this.supports(f));
    
    if (needed.length === 0) return;
    
    const polyfills = {
      'promises': () => import('promise-polyfill'),
      'fetch': () => import('whatwg-fetch'),
      'intersectionObserver': () => import('intersection-observer'),
      'customElements': () => import('@webcomponents/custom-elements')
    };
    
    const loadPromises = needed
      .filter(feature => polyfills[feature])
      .map(feature => polyfills[feature]());
    
    await Promise.all(loadPromises);
  }
}

export const featureDetection = new FeatureDetectionService();

2. Component Enhancement Pattern

// Progressive component enhancement
class EnhancedComponent {
  constructor(element) {
    this.element = element;
    this.features = window.featureDetection;
    
    // Base functionality that works everywhere
    this.initBase();
    
    // Progressive enhancements
    if (this.features.supports('intersectionObserver')) {
      this.addLazyLoading();
    }
    
    if (this.features.supports('customElements')) {
      this.upgradeToWebComponent();
    }
    
    if (this.features.supports('grid')) {
      this.element.classList.add('grid-layout');
    }
  }
  
  initBase() {
    // Basic functionality
    this.element.addEventListener('click', this.handleClick.bind(this));
  }
  
  addLazyLoading() {
    const images = this.element.querySelectorAll('img[data-src]');
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          observer.unobserve(img);
        }
      });
    });
    
    images.forEach(img => observer.observe(img));
  }
  
  upgradeToWebComponent() {
    // Convert to custom element if supported
    customElements.define('enhanced-component', class extends HTMLElement {
      connectedCallback() {
        this.innerHTML = this.element.innerHTML;
        // Add shadow DOM and enhanced functionality
      }
    });
  }
}

3. Compatibility Layer

// utils/compatibility.js
export const compat = {
  // Safe event listener with passive support detection
  addEventListener(element, event, handler, options = {}) {
    let supportsPassive = false;
    try {
      const opts = Object.defineProperty({}, 'passive', {
        get: () => { supportsPassive = true; }
      });
      window.addEventListener('testPassive', null, opts);
      window.removeEventListener('testPassive', null, opts);
    } catch (e) {}
    
    const finalOptions = supportsPassive ? options : !!options.capture;
    element.addEventListener(event, handler, finalOptions);
  },
  
  // Cross-browser animation frame
  requestAnimationFrame(callback) {
    const raf = window.requestAnimationFrame ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                (cb => setTimeout(cb, 1000 / 60));
    
    return raf.call(window, callback);
  },
  
  // Safe querySelector with fallback
  querySelector(selector, context = document) {
    try {
      return context.querySelector(selector);
    } catch (e) {
      // Fallback for older browsers
      if (selector.startsWith('#')) {
        return document.getElementById(selector.slice(1));
      } else if (selector.startsWith('.')) {
        return context.getElementsByClassName(selector.slice(1))[0];
      } else {
        return context.getElementsByTagName(selector)[0];
      }
    }
  },
  
  // Cross-browser fetch
  async fetch(url, options = {}) {
    if (window.fetch) {
      return window.fetch(url, options);
    }
    
    // XMLHttpRequest fallback
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open(options.method || 'GET', url);
      
      Object.keys(options.headers || {}).forEach(key => {
        xhr.setRequestHeader(key, options.headers[key]);
      });
      
      xhr.onload = () => {
        resolve({
          ok: xhr.status >= 200 && xhr.status < 300,
          status: xhr.status,
          json: () => Promise.resolve(JSON.parse(xhr.responseText)),
          text: () => Promise.resolve(xhr.responseText)
        });
      };
      
      xhr.onerror = () => reject(new Error('Network request failed'));
      xhr.send(options.body);
    });
  }
};

Browser-Specific CSS Hacks (Use with Caution)

Sometimes you need to target specific browsers. Here are some techniques (use sparingly):

/* Internet Explorer 10-11 */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
  .ie10-11 {
    /* IE10-11 specific styles */
  }
}

/* Edge (pre-Chromium) */
@supports (-ms-ime-align: auto) {
  .edge-legacy {
    /* Edge Legacy specific styles */
  }
}

/* Safari only */
@media not all and (min-resolution:.001dpcm) { 
  @supports (-webkit-appearance:none) {
    .safari-only {
      /* Safari specific styles */
    }
  }
}

/* Firefox only */
@-moz-document url-prefix() {
  .firefox-only {
    /* Firefox specific styles */
  }
}

/* Chrome only */
@media screen and (-webkit-min-device-pixel-ratio:0) and (min-resolution:.001dpcm) {
  .chrome-only {
    /* Chrome specific styles */
  }
}

JavaScript Browser Detection (When Necessary)

// Modern browser detection utility
const browserDetect = {
  isChrome: () => !!window.chrome && !!window.chrome.webstore,
  isFirefox: () => typeof InstallTrigger !== 'undefined',
  isSafari: () => /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
  isEdge: () => !!window.StyleMedia || !!window.MSInputMethodContext,
  isIE: () => /*@cc_on!@*/false || !!document.documentMode,
  
  // Get browser info
  getBrowserInfo() {
    const ua = navigator.userAgent;
    let tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
    
    if (/trident/i.test(M[1])) {
      tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
      return { name: 'IE', version: (tem[1] || '') };
    }
    
    if (M[1] === 'Chrome') {
      tem = ua.match(/\bOPR|Edge\/(\d+)/);
      if (tem != null) {
        return { name: 'Edge', version: tem[1] };
      }
    }
    
    M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
    
    if ((tem = ua.match(/version\/(\d+)/i)) != null) {
      M.splice(1, 1, tem[1]);
    }
    
    return {
      name: M[0],
      version: M[1]
    };
  }
};

Performance Considerations

Compatibility code can impact performance. Here's how to optimize:

1. Conditional Loading

// Load polyfills only when needed
function loadPolyfills() {
  const polyfills = [];
  
  // Check and load what's needed
  if (!window.Promise) {
    polyfills.push(loadScript('/polyfills/promise.js'));
  }
  
  if (!window.fetch) {
    polyfills.push(loadScript('/polyfills/fetch.js'));
  }
  
  if (!Element.prototype.closest) {
    polyfills.push(loadScript('/polyfills/element-closest.js'));
  }
  
  return Promise.all(polyfills);
}

// Utility to load scripts dynamically
function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// Initialize app after polyfills
loadPolyfills().then(() => {
  initializeApp();
});

2. Bundle Splitting

// webpack.config.js for modern/legacy bundles
module.exports = [
  // Modern browsers bundle
  {
    entry: './src/index.js',
    output: {
      filename: 'modern.js',
    },
    target: ['web', 'es2017'],
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', {
                  targets: { esmodules: true },
                  useBuiltIns: 'usage',
                  corejs: 3
                }]
              ]
            }
          }
        }
      ]
    }
  },
  
  // Legacy browsers bundle
  {
    entry: './src/index.js',
    output: {
      filename: 'legacy.js',
    },
    target: ['web', 'es5'],
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', {
                  targets: '> 0.25%, not dead',
                  useBuiltIns: 'usage',
                  corejs: 3
                }]
              ]
            }
          }
        }
      ]
    }
  }
];

// HTML differential loading


Practical Exercise: Building a Compatible Image Gallery

Let's build an image gallery that works across browsers:

// gallery.js - A progressively enhanced image gallery
class CompatibleGallery {
  constructor(container) {
    this.container = container;
    this.images = container.querySelectorAll('img');
    this.currentIndex = 0;
    
    // Initialize with appropriate features
    this.init();
  }
  
  init() {
    // Basic functionality for all browsers
    this.setupBasicGallery();
    
    // Progressive enhancements
    if (this.supportsIntersectionObserver()) {
      this.setupLazyLoading();
    }
    
    if (this.supportsTouchEvents()) {
      this.setupTouchNavigation();
    }
    
    if (this.supportsGrid()) {
      this.setupGridLayout();
    }
    
    if (this.supportsCustomProperties()) {
      this.setupThemeCustomization();
    }
  }
  
  setupBasicGallery() {
    // Create navigation buttons
    const prevBtn = this.createButton('Previous', () => this.navigate(-1));
    const nextBtn = this.createButton('Next', () => this.navigate(1));
    
    this.container.appendChild(prevBtn);
    this.container.appendChild(nextBtn);
    
    // Basic keyboard navigation
    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowLeft') this.navigate(-1);
      if (e.key === 'ArrowRight') this.navigate(1);
    });
  }
  
  setupLazyLoading() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          if (img.dataset.src) {
            img.src = img.dataset.src;
            img.onload = () => img.classList.add('loaded');
          }
          observer.unobserve(img);
        }
      });
    }, { threshold: 0.1 });
    
    this.images.forEach(img => {
      if (img.dataset.src) {
        observer.observe(img);
      }
    });
  }
  
  setupTouchNavigation() {
    let touchStartX = 0;
    let touchEndX = 0;
    
    this.container.addEventListener('touchstart', (e) => {
      touchStartX = e.changedTouches[0].screenX;
    }, false);
    
    this.container.addEventListener('touchend', (e) => {
      touchEndX = e.changedTouches[0].screenX;
      this.handleSwipe();
    }, false);
    
    this.handleSwipe = () => {
      const swipeThreshold = 50;
      const diff = touchStartX - touchEndX;
      
      if (Math.abs(diff) > swipeThreshold) {
        if (diff > 0) {
          this.navigate(1); // Swipe left, go next
        } else {
          this.navigate(-1); // Swipe right, go previous
        }
      }
    };
  }
  
  setupGridLayout() {
    this.container.classList.add('grid-supported');
    
    // Add CSS through JavaScript for browsers that support grid
    const style = document.createElement('style');
    style.textContent = `
      .grid-supported {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
        gap: 1rem;
      }
    `;
    document.head.appendChild(style);
  }
  
  setupThemeCustomization() {
    // Add theme toggle if CSS custom properties are supported
    const themeToggle = this.createButton('Toggle Theme', () => {
      document.body.classList.toggle('dark-theme');
    });
    
    this.container.appendChild(themeToggle);
    
    // Add CSS variables
    const style = document.createElement('style');
    style.textContent = `
      :root {
        --gallery-bg: #ffffff;
        --gallery-text: #333333;
        --gallery-border: #dddddd;
      }
      
      .dark-theme {
        --gallery-bg: #333333;
        --gallery-text: #ffffff;
        --gallery-border: #555555;
      }
      
      .gallery {
        background-color: var(--gallery-bg);
        color: var(--gallery-text);
        border-color: var(--gallery-border);
      }
    `;
    document.head.appendChild(style);
  }
  
  navigate(direction) {
    this.currentIndex += direction;
    if (this.currentIndex < 0) this.currentIndex = this.images.length - 1;
    if (this.currentIndex >= this.images.length) this.currentIndex = 0;
    
    this.showImage(this.currentIndex);
  }
  
  showImage(index) {
    this.images.forEach((img, i) => {
      img.style.display = i === index ? 'block' : 'none';
    });
  }
  
  createButton(text, onClick) {
    const button = document.createElement('button');
    button.textContent = text;
    button.onclick = onClick;
    return button;
  }
  
  // Feature detection methods
  supportsIntersectionObserver() {
    return 'IntersectionObserver' in window;
  }
  
  supportsTouchEvents() {
    return 'ontouchstart' in window;
  }
  
  supportsGrid() {
    return CSS && CSS.supports && CSS.supports('display', 'grid');
  }
  
  supportsCustomProperties() {
    return CSS && CSS.supports && CSS.supports('--test', '0');
  }
}

// Usage
document.addEventListener('DOMContentLoaded', () => {
  const gallery = document.querySelector('.gallery');
  if (gallery) {
    new CompatibleGallery(gallery);
  }
});

Summary and Best Practices

Key Takeaways

Compatibility Checklist

Tools and Resources