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
- Mobile-First - PWA strategies
- Accessibility - Performance as a11y
- Component Library - Optimized components
- Multilingual - Font optimization
- Search & Discovery - Pagefind static search
- Audience Journeys - Offline-first user flows