number;
}
async function fetchWithTimeout({
url,
method = "GET",
headers = {},
body,
timeout = 5000,
}: FetchOptions): Promise<Response> {
const controller = new AbortController();
const signal = controller.signal;
const timer = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} finally {
clearTimeout(timer);
}
}
**Rationale:**
- **AbortController:** Provides a standard way to cancel requests, preventing memory leaks and unnecessary network traffic.
- **Timeout Handling:** Native `fetch` does not support timeouts by default. Wrapping it with `AbortController` ensures requests do not hang indefinitely.
- **Type Safety:** Using TypeScript interfaces ensures type safety for request options and responses.
### Step 2: Client-Side Storage with `IndexedDB`
For complex data storage needs, `IndexedDB` is the most robust solution. While the native API is verbose, using a lightweight wrapper like `idb` simplifies usage without the overhead of larger libraries.
**Implementation:**
```typescript
import { openDB, DBSchema, IDBPDatabase } from "idb";
interface AppDB extends DBSchema {
notes: {
key: string;
value: { id: string; title: string; content: string; timestamp: number };
indexes: { byTimestamp: number };
};
}
async function initDB(): Promise<IDBPDatabase<AppDB>> {
return openDB<AppDB>("app-db", 1, {
upgrade(db) {
const store = db.createObjectStore("notes", { keyPath: "id" });
store.createIndex("byTimestamp", "timestamp");
},
});
}
async function saveNote(note: { id: string; title: string; content: string }) {
const db = await initDB();
await db.put("notes", { ...note, timestamp: Date.now() });
}
async function getNotes(): Promise<AppDB["notes"]["value"][]> {
const db = await initDB();
return db.getAllFromIndex("notes", "byTimestamp");
}
Rationale:
- Structured Data:
IndexedDB supports complex data types and indexing, making it suitable for applications with significant data storage requirements.
- Asynchronous API: Unlike
localStorage, IndexedDB is asynchronous, preventing UI blocking.
- Lightweight Wrapper: Using
idb reduces boilerplate code while maintaining direct access to the underlying API.
Step 3: Background Processing with Web Workers
For CPU-intensive tasks, Web Workers prevent blocking the main thread, ensuring a smooth user experience.
Implementation:
// worker.ts
import { expose } from "comlink";
const workerApi = {
async processLargeDataset(data: number[]): Promise<number> {
// Simulate heavy computation
return data.reduce((acc, val) => acc + val, 0);
},
};
expose(workerApi);
// main.ts
import { wrap } from "comlink";
import Worker from "./worker?worker";
const worker = wrap<typeof workerApi>(new Worker());
async function handleDataProcessing() {
const data = Array.from({ length: 1000000 }, (_, i) => i);
const result = await worker.processLargeDataset(data);
console.log("Processed result:", result);
}
Rationale:
- Thread Isolation: Web Workers run in a separate thread, preventing long-running tasks from blocking the main thread.
- Comlink: Simplifies worker communication by providing a proxy-based API, making it easier to use than the native
postMessage API.
- Performance: Offloading heavy computation improves the application's responsiveness and Core Web Vitals.
Step 4: Visibility Detection with IntersectionObserver
IntersectionObserver provides an efficient way to detect when elements enter or exit the viewport.
Implementation:
function setupLazyLoading(selector: string) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src || "";
observer.unobserve(img);
}
});
},
{ rootMargin: "200px" }
);
document.querySelectorAll(selector).forEach((el) => observer.observe(el));
}
Rationale:
- Performance:
IntersectionObserver is optimized by the browser, avoiding the performance issues associated with scroll event listeners.
- Lazy Loading: Delays loading of off-screen resources until they are needed, improving initial page load times.
- Root Margin: Allows triggering actions before the element enters the viewport, ensuring a seamless user experience.
Pitfall Guide
-
Misusing localStorage for Sensitive Data
- Explanation:
localStorage is accessible to any JavaScript running on the page, including third-party scripts. Storing sensitive data like auth tokens exposes them to XSS attacks.
- Fix: Use
HttpOnly cookies for session management or IndexedDB for non-sensitive data.
-
Blocking the Main Thread with Synchronous Operations
- Explanation: Performing heavy computations or large data processing on the main thread can cause UI jank and poor performance.
- Fix: Offload heavy tasks to Web Workers or use
requestIdleCallback for non-critical work.
-
Ignoring AbortController for Long-Running Requests
- Explanation: Failing to cancel requests can lead to memory leaks and unnecessary network traffic, especially in single-page applications.
- Fix: Always use
AbortController to manage request lifecycles, especially for components that may unmount before the request completes.
-
Overusing IntersectionObserver
- Explanation: Creating too many observers can impact performance, especially on pages with many elements.
- Fix: Use a single observer for multiple elements and unobserve elements once they are no longer needed.
-
Neglecting Service Worker Updates
- Explanation: Failing to update Service Workers can result in users receiving stale content.
- Fix: Implement a strategy for updating Service Workers, such as using
workbox to handle caching and updates automatically.
-
Assuming All Browsers Support Every API
- Explanation: While most modern browsers support the APIs discussed, some older browsers may not.
- Fix: Use feature detection and provide fallbacks for unsupported APIs.
-
Using localStorage for Large Data Sets
- Explanation:
localStorage has a limited storage capacity and is synchronous, which can block the main thread.
- Fix: Use
IndexedDB for larger data sets or complex storage needs.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple key-value storage | localStorage | Easy to use, synchronous | Low |
| Complex data storage | IndexedDB | Asynchronous, supports indexing | Medium |
| Session management | HttpOnly cookies | Secure, prevents XSS | Low |
| Heavy computation | Web Workers | Prevents UI blocking | Medium |
| Visibility detection | IntersectionObserver | Performance optimized | Low |
| Network requests | fetch + AbortController | Native, supports streaming | Low |
Configuration Template
// vite.config.ts
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
VitePWA({
registerType: "autoUpdate",
workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg}"],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: "NetworkFirst",
options: {
cacheName: "api-cache",
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24, // 24 hours
},
},
},
],
},
}),
],
});
Quick Start Guide
- Audit Dependencies: Review your
package.json and identify libraries that can be replaced with native APIs.
- Implement Native APIs: Replace identified libraries with native equivalents using the examples provided.
- Test Thoroughly: Ensure all functionality works as expected across different browsers and devices.
- Monitor Performance: Use tools like Lighthouse to measure improvements in Core Web Vitals.
- Iterate: Continuously evaluate and optimize your use of native APIs as the platform evolves.