Emergency Hotline: Call 1-844-363-1423 (United We Dream Hotline)
ICE Encounter

Performance as Equity

For a user base frequently relying on low-bandwidth 3G connections and older hardware, aggressive performance budgets are a matter of equity and access. A user on a prepaid plan cannot afford to download megabytes of unoptimized assets.

A 223-page site heavily laden with interactive assessment tools, massive multilingual datasets, and high-resolution PDF printables presents significant performance challenges. The technical architecture must prioritize absolute speed and offline resilience to protect users operating in crisis environments.


Core Web Vitals Targets

Metrics

Metric Target Why It Matters
LCP < 2.5s User sees main content quickly
FID/INP < 100ms Interface responds to panic taps
CLS < 0.1 No accidental mis-taps from shifts

Real-World Context

These targets must be met at the 75th percentile of mobile users—not just in lab conditions on developer machines.

// Monitor real user metrics
if ('web-vital' in window) {
  import('web-vitals').then(({ getCLS, getFID, getLCP }) => {
    getCLS(console.log);
    getFID(console.log);
    getLCP(console.log);
  });
}

Image Optimization

Modern Formats

<picture>
  <source srcset="/images/rights-card.avif" type="image/avif">
  <source srcset="/images/rights-card.webp" type="image/webp">
  <img src="/images/rights-card.jpg"
       alt="Know Your Rights card"
       loading="lazy"
       width="600"
       height="400">
</picture>

Responsive Images

<img srcset="/images/hero-400.webp 400w,
             /images/hero-800.webp 800w,
             /images/hero-1200.webp 1200w"
     sizes="(max-width: 600px) 100vw,
            (max-width: 1200px) 50vw,
            800px"
     src="/images/hero-800.webp"
     alt="Community support"
     loading="lazy">

Loading Strategy

Image Type Strategy Rationale
Hero images Eager load Critical for LCP
Below fold loading="lazy" Save initial bandwidth
Icons Inline SVG No network request
Decorative loading="lazy" Non-essential
<!-- Critical hero - no lazy loading -->
<img src="/images/emergency-hero.webp"
     alt="Emergency resources"
     fetchpriority="high">

<!-- Below fold - lazy load -->
<img src="/images/guide-preview.webp"
     alt="Rights guide preview"
     loading="lazy">

CSS Optimization

Critical CSS Inlining

<head>
  <!-- Inline critical CSS for immediate render -->
  <style>
    /* Critical above-the-fold styles */
    :root {
      --color-primary: #2563eb;
      --color-text: #1f2937;
    }
    body {
      font-family: system-ui, sans-serif;
      margin: 0;
    }
    .hero { /* Hero styles */ }
    .nav { /* Navigation styles */ }
  </style>

  <!-- Non-critical CSS loaded async -->
  <link rel="preload" href="/css/main.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript>
    <link rel="stylesheet" href="/css/main.css">
  </noscript>
</head>

11ty CSS Bundling

// .eleventy.js
const CleanCSS = require("clean-css");

module.exports = function(eleventyConfig) {
  // Minify CSS
  eleventyConfig.addFilter("cssmin", function(code) {
    return new CleanCSS({}).minify(code).styles;
  });

  // Bundle CSS per page
  eleventyConfig.addBundle("css");
};
{# In layout template #}
<style>
  {% getBundle "css" | cssmin | safe %}
</style>

Per-Page Bundles

{# Only include GIS CSS on mapping pages #}
{% if tags and 'mapping-tools' in tags %}
  {% css %}
    @import "components/map.css";
    @import "components/legend.css";
  {% endcss %}
{% endif %}

Font Optimization

Font Subsetting

Large multilingual fonts must be subset to reduce payload:

/* Latin subset - ~20KB */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                 U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

/* Cyrillic subset - loaded on demand */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/inter-cyrillic.woff2') format('woff2');
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* Arabic subset - loaded on demand */
@font-face {
  font-family: 'Noto Sans Arabic';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/noto-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011,
                 U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
}

Font Loading Strategy

<!-- Preload critical fonts -->
<link rel="preload"
      href="/fonts/inter-latin.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

<!-- Font display swap prevents FOIT -->
<style>
  @font-face {
    font-family: 'Inter';
    font-display: swap;
    /* ... */
  }
</style>

CJK Font Strategy

Chinese, Japanese, Korean fonts are large (several MB). Use Google Fonts API for automatic subsetting:

<!-- Only downloads glyphs actually used on page -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap"
      rel="stylesheet">

Or self-host with aggressive subsetting:

# Generate subset with pyftsubset
pyftsubset NotoSansSC-Regular.ttf \
  --unicodes="U+4E00-9FFF" \
  --flavor=woff2 \
  --output-file=noto-sc-subset.woff2

JavaScript Optimization

Defer Non-Critical Scripts

<!-- Critical: inline or sync -->
<script>
  // Minimal inline JS for emergency features
  document.getElementById('quick-exit').onclick = () => {
    window.location.replace('https://weather.com');
  };
</script>

<!-- Non-critical: defer -->
<script src="/js/analytics.js" defer></script>
<script src="/js/map-loader.js" defer></script>

Dynamic Imports

// Only load heavy mapping libraries when needed
const mapContainer = document.getElementById('map');

if (mapContainer) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        import('./map-module.js').then(({ initMap }) => {
          initMap(mapContainer);
        });
        observer.disconnect();
      }
    });
  });

  observer.observe(mapContainer);
}

Code Splitting in 11ty

// _data/jsBundle.js
module.exports = {
  // Define bundles per page type
  emergency: ['quick-exit.js', 'hotline.js'],
  mapping: ['leaflet.js', 'map-controls.js', 'geolocation.js'],
  forms: ['validation.js', 'autosave.js']
};
{# Only include needed JS #}
{% if tags and 'emergency' in tags %}
  <script src="/js/emergency-bundle.js" defer></script>
{% endif %}

{% if tags and 'mapping-tools' in tags %}
  <script src="/js/mapping-bundle.js" defer></script>
{% endif %}

Network Optimization

Service Worker Caching

// sw.js - Cache strategies by content type
const CACHE_NAME = 'ice-advocacy-v1';

const CACHE_STRATEGIES = {
  // App shell - cache first
  shell: [
    '/',
    '/css/main.css',
    '/js/main.js',
    '/offline.html'
  ],

  // Rights content - stale while revalidate
  content: /\/(know-your-rights|emergency)\//,

  // Dynamic data - network first
  dynamic: /\/(api|flight-tracking)\//
};

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // App shell: cache first
  if (CACHE_STRATEGIES.shell.includes(url.pathname)) {
    event.respondWith(cacheFirst(request));
    return;
  }

  // Rights content: stale while revalidate
  if (CACHE_STRATEGIES.content.test(url.pathname)) {
    event.respondWith(staleWhileRevalidate(request));
    return;
  }

  // Dynamic: network first with cache fallback
  if (CACHE_STRATEGIES.dynamic.test(url.pathname)) {
    event.respondWith(networkFirst(request));
    return;
  }
});

async function cacheFirst(request) {
  const cached = await caches.match(request);
  return cached || fetch(request);
}

async function staleWhileRevalidate(request) {
  const cached = await caches.match(request);
  const fetchPromise = fetch(request).then(response => {
    const cache = caches.open(CACHE_NAME);
    cache.then(c => c.put(request, response.clone()));
    return response;
  });
  return cached || fetchPromise;
}

async function networkFirst(request) {
  try {
    return await fetch(request);
  } catch {
    return caches.match(request);
  }
}

Preconnect and Prefetch

<!-- Preconnect to critical origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- DNS prefetch for likely destinations -->
<link rel="dns-prefetch" href="https://api.mapbox.com">

<!-- Prefetch likely next pages -->
<link rel="prefetch" href="/know-your-rights/">
<link rel="prefetch" href="/emergency/">

11ty Build Optimization

Scaling a static site generator (SSG) like Eleventy to 223 pages requires aggressive optimization of the build pipeline to prevent developer friction and CI/CD bottlenecks.

Incremental Builds

# Enable incremental builds for development
npx @11ty/eleventy --incremental --serve

# Production build with full compilation
npx @11ty/eleventy

When a Markdown file is altered, 11ty rebuilds only that specific file and its direct dependents, reducing build times from minutes to milliseconds.

Smart Collection Management

# In frontmatter - targeted rebuilds
---
eleventyImport:
  collections: ["rights", "printables"]
---

Using eleventyImport ensures changes to a resource tag properly trigger targeted rebuilds for associated hub pages without necessitating a full-site compilation.

API Fetch Caching

For pages relying on external data (e.g., pulling facility data from TRAC or ICE APIs):

// eleventy.config.js
const EleventyFetch = require("@11ty/eleventy-fetch");

module.exports = function(eleventyConfig) {
  eleventyConfig.addGlobalData("facilityData", async () => {
    try {
      const data = await EleventyFetch(
        "https://api.ice.gov/facilities.json",
        {
          duration: "1d", // Cache for 1 day
          type: "json",
          directory: ".cache"
        }
      );
      return data;
    } catch (err) {
      console.error("Failed to fetch facility data:", err);
      // Return cached data or empty array
      return [];
    }
  });
};

This prevents build pipeline failures if the external API throttles requests, rate-limits, or experiences downtime.

Responsive Image Pipeline

// eleventy.config.js
const Image = require("@11ty/eleventy-img");

async function imageShortcode(src, alt, sizes = "100vw") {
  const metadata = await Image(src, {
    widths: [400, 800, 1200],
    formats: ["avif", "webp", "jpeg"],
    outputDir: "./_site/img/",
    urlPath: "/img/",
    filenameFormat: (id, src, width, format) => {
      const name = path.basename(src, path.extname(src));
      return `${name}-${width}w.${format}`;
    }
  });

  const imageAttributes = {
    alt,
    sizes,
    loading: "lazy",
    decoding: "async"
  };

  return Image.generateHTML(metadata, imageAttributes);
}

module.exports = function(eleventyConfig) {
  eleventyConfig.addNunjucksAsyncShortcode("image", imageShortcode);
};

Usage in templates:

{% image "./src/images/rights-card.jpg", "Know Your Rights card", "(max-width: 768px) 100vw, 800px" %}

PWA & Offline-First Architecture

The most critical technical mandate for this advocacy platform is its ability to function flawlessly during network outages, cell tower congestion, or deliberate communications jamming—frequent occurrences during field observations, protests, or crisis events.

Service Worker Registration

// In main.js or inline in layout
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW registered:', registration.scope);
      })
      .catch(err => {
        console.error('SW registration failed:', err);
      });
  });
}

Workbox Configuration

// sw.js - Using Workbox for advanced caching
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');

const { precacheAndRoute, cleanupOutdatedCaches } = workbox.precaching;
const { registerRoute } = workbox.routing;
const { StaleWhileRevalidate, CacheFirst, NetworkFirst } = workbox.strategies;
const { ExpirationPlugin } = workbox.expiration;

// Clean up old caches
cleanupOutdatedCaches();

// Precache critical crisis assets at install time
precacheAndRoute([
  { url: '/', revision: '1' },
  { url: '/emergency/', revision: '1' },
  { url: '/emergency/red-card/', revision: '1' },
  { url: '/emergency/hotlines/', revision: '1' },
  { url: '/offline.html', revision: '1' },
  { url: '/css/critical.css', revision: '1' },
  { url: '/js/core.js', revision: '1' },
  { url: '/images/red-card-en.pdf', revision: '1' },
  { url: '/images/red-card-es.pdf', revision: '1' }
]);

Caching Strategies by Content Type

// HTML pages - Network First with cache fallback
registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: 'pages-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 7 * 24 * 60 * 60 // 1 week
      })
    ]
  })
);

// Rights content - Stale While Revalidate
registerRoute(
  ({ url }) => url.pathname.match(/^\/(rights|legal|emergency)\//),
  new StaleWhileRevalidate({
    cacheName: 'rights-content',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days
      })
    ]
  })
);

// Static assets - Cache First
registerRoute(
  ({ request }) =>
    request.destination === 'style' ||
    request.destination === 'script' ||
    request.destination === 'font',
  new CacheFirst({
    cacheName: 'static-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 365 * 24 * 60 * 60 // 1 year
      })
    ]
  })
);

// Images - Cache First with size limit
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 30 * 24 * 60 * 60,
        purgeOnQuotaError: true
      })
    ]
  })
);

// PDF downloads - Cache on demand
registerRoute(
  ({ url }) => url.pathname.endsWith('.pdf'),
  new CacheFirst({
    cacheName: 'pdf-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 30,
        maxAgeSeconds: 90 * 24 * 60 * 60 // 90 days
      })
    ]
  })
);

Offline Fallback Page

// Handle offline navigation
self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => {
        return caches.match('/offline.html');
      })
    );
  }
});
<!-- /offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>You're Offline - Immigration Rights Guide</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 2rem; text-align: center; }
    h1 { color: #d32f2f; }
    .cached-pages { text-align: left; margin: 2rem auto; max-width: 400px; }
  </style>
</head>
<body>
  <h1>You're Currently Offline</h1>
  <p>But don't worry — critical emergency resources are still available.</p>

  <div class="cached-pages">
    <h2>Available Offline:</h2>
    <ul id="cached-list">
      <li><a href="/emergency/">Emergency Help</a></li>
      <li><a href="/emergency/red-card/">Red Cards</a></li>
      <li><a href="/emergency/hotlines/">Crisis Hotlines</a></li>
    </ul>
  </div>

  <p>Full content will be available when you reconnect.</p>
</body>
</html>

IndexedDB for Offline Field Data

For Legal Observers entering field data offline, the IndexedDB API provides a secure, local NoSQL storage solution.

Database Setup

// db.js - IndexedDB wrapper for field observations
const DB_NAME = 'advocacy-fieldwork';
const DB_VERSION = 1;

function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;

      // Store for field observations
      if (!db.objectStoreNames.contains('observations')) {
        const store = db.createObjectStore('observations', {
          keyPath: 'id',
          autoIncrement: true
        });
        store.createIndex('timestamp', 'timestamp');
        store.createIndex('synced', 'synced');
      }

      // Store for incident reports
      if (!db.objectStoreNames.contains('incidents')) {
        const store = db.createObjectStore('incidents', {
          keyPath: 'id',
          autoIncrement: true
        });
        store.createIndex('timestamp', 'timestamp');
        store.createIndex('type', 'type');
        store.createIndex('synced', 'synced');
      }
    };
  });
}

Saving Observations Offline

async function saveObservation(data) {
  const db = await openDatabase();
  const tx = db.transaction('observations', 'readwrite');
  const store = tx.objectStore('observations');

  const observation = {
    ...data,
    timestamp: Date.now(),
    synced: false,
    deviceId: getDeviceFingerprint()
  };

  return new Promise((resolve, reject) => {
    const request = store.add(observation);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Get all unsynced observations
async function getUnsyncedObservations() {
  const db = await openDatabase();
  const tx = db.transaction('observations', 'readonly');
  const store = tx.objectStore('observations');
  const index = store.index('synced');

  return new Promise((resolve, reject) => {
    const request = index.getAll(IDBKeyRange.only(false));
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Background Sync API

Complex form inputs are saved to IndexedDB and synced to the secure server only once a reliable, trusted connection is detected:

// Register for background sync
async function requestBackgroundSync() {
  if ('serviceWorker' in navigator && 'SyncManager' in window) {
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('sync-observations');
    console.log('Background sync registered');
  } else {
    // Fallback: try immediate sync
    syncObservations();
  }
}

// In service worker - handle sync event
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-observations') {
    event.waitUntil(syncObservationsToServer());
  }
});

async function syncObservationsToServer() {
  const observations = await getUnsyncedObservations();

  for (const observation of observations) {
    try {
      const response = await fetch('/api/observations', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(observation)
      });

      if (response.ok) {
        await markAsSynced(observation.id);
      }
    } catch (err) {
      console.error('Sync failed for observation:', observation.id);
      // Will retry on next sync event
    }
  }
}

async function markAsSynced(id) {
  const db = await openDatabase();
  const tx = db.transaction('observations', 'readwrite');
  const store = tx.objectStore('observations');

  const observation = await store.get(id);
  observation.synced = true;
  observation.syncedAt = Date.now();
  await store.put(observation);
}

Network Status Indicator

// UI component showing sync status
function updateNetworkStatus() {
  const indicator = document.getElementById('network-status');

  if (navigator.onLine) {
    indicator.textContent = 'Online';
    indicator.className = 'status-online';
    requestBackgroundSync();
  } else {
    indicator.textContent = 'Offline - Data saved locally';
    indicator.className = 'status-offline';
  }
}

window.addEventListener('online', updateNetworkStatus);
window.addEventListener('offline', updateNetworkStatus);
document.addEventListener('DOMContentLoaded', updateNetworkStatus);

Performance Budgets

Budget Targets

Resource Budget Rationale
HTML < 50KB Initial render
CSS < 50KB Style blocking
JS < 100KB Parse time
Images < 500KB Per page
Fonts < 100KB Per language
Total < 800KB 3G in 3s

CI/CD Integration

# .github/workflows/performance.yml
name: Performance Budget

on: [push, pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build site
        run: npm run build

      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v10
        with:
          configPath: './lighthouserc.json'
          uploadArtifacts: true

      - name: Check budgets
        run: |
          # Fail if LCP > 2.5s or CLS > 0.1
          npm run check-budgets
// lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", {"minScore": 0.9}],
        "largest-contentful-paint": ["error", {"maxNumericValue": 2500}],
        "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}],
        "interactive": ["error", {"maxNumericValue": 3500}]
      }
    }
  }
}

Real User Monitoring

RUM Implementation

// Capture real performance data
function sendMetrics(metric) {
  // Privacy-preserving: no user identifiers
  const data = {
    name: metric.name,
    value: metric.value,
    rating: metric.rating, // 'good', 'needs-improvement', 'poor'
    page: window.location.pathname,
    connection: navigator.connection?.effectiveType || 'unknown',
    device: /Mobile/.test(navigator.userAgent) ? 'mobile' : 'desktop'
  };

  // Use sendBeacon for reliability
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/metrics', JSON.stringify(data));
  }
}

// Import web-vitals library
import { onCLS, onFID, onLCP, onINP } from 'web-vitals';

onCLS(sendMetrics);
onFID(sendMetrics);
onLCP(sendMetrics);
onINP(sendMetrics);

Connection-Aware Loading

// Adapt to network conditions
const connection = navigator.connection || {};
const saveData = connection.saveData;
const slowConnection = connection.effectiveType === '2g' ||
                       connection.effectiveType === 'slow-2g';

if (saveData || slowConnection) {
  // Load minimal assets
  document.body.classList.add('reduced-data');

  // Skip non-essential images
  document.querySelectorAll('img[data-optional]').forEach(img => {
    img.remove();
  });

  // Use text-only map fallback
  document.querySelectorAll('.map-container').forEach(map => {
    map.innerHTML = '<a href="/find-help/directory/">View text directory</a>';
  });
}

Testing Checklist

Performance

  • [ ] LCP < 2.5s on 3G
  • [ ] FID/INP < 100ms
  • [ ] CLS < 0.1
  • [ ] Total page weight < 800KB

Optimization

  • [ ] Images in WebP/AVIF
  • [ ] Critical CSS inlined
  • [ ] Fonts subset by language
  • [ ] JS deferred/split

Monitoring

  • [ ] Lighthouse CI in pipeline
  • [ ] RUM collecting metrics
  • [ ] Performance budgets enforced
  • [ ] Connection-aware loading

Related Resources