Browser APIs - Web Workers, Service Workers, IndexedDB - Pytania Rekrutacyjne 2026
Na rozmowie rekrutacyjnej pada pytanie: "Jak zoptymalizujesz ciężkie obliczenia, żeby nie blokować UI?". Kandydat odpowiada: "Użyję setTimeout". To odpowiedź, która pokazuje nieznajomość nowoczesnych Browser APIs. W 2026 roku Web Workers, Service Workers i IndexedDB to standard - bez nich nie zbudujesz wydajnej aplikacji offline-first ani PWA.
W tym przewodniku znajdziesz wszystko o kluczowych Browser APIs na rozmowę rekrutacyjną - od Web Workers przez Service Workers po IndexedDB. Każda sekcja zawiera kod, diagramy i pytania rekrutacyjne z odpowiedziami.
Jak odpowiedzieć na pytania o Browser APIs
Odpowiedź w 30 sekund
"Browser APIs dzielę na trzy główne kategorie. Web Workers służą do ciężkich obliczeń w tle - działają w osobnym wątku, nie blokują UI, komunikują się przez postMessage. Service Workers to proxy sieciowe - działają między aplikacją a siecią, umożliwiają caching, offline mode i push notifications. IndexedDB to baza danych w przeglądarce - asynchroniczna, transakcyjna, idealna dla dużych ilości strukturalnych danych. Razem tworzą fundament PWA."
Odpowiedź w 2 minuty
Browser APIs to zestaw interfejsów pozwalających na zaawansowane operacje w przeglądarce, które wykraczają poza podstawowy JavaScript.
Web Workers rozwiązują problem single-threaded JavaScript. Gdy masz ciężkie obliczenia - parsowanie dużego JSON, przetwarzanie obrazu, kryptografię - blokujesz main thread i UI się zacina. Web Worker działa w osobnym wątku, wykonuje obliczenia w tle i zwraca wynik przez postMessage. Nie ma dostępu do DOM, ale ma dostęp do większości Web APIs. Dane między main thread a workerem są kopiowane, chyba że użyjesz Transferable Objects.
Service Workers to zupełnie inna koncepcja - działają jako programowalne proxy między aplikacją a siecią. Rejestrujesz Service Worker, który przechwytuje wszystkie żądania fetch i decyduje: zwrócić z cache'a? Pobrać z sieci? Użyć strategii stale-while-revalidate? Service Worker żyje niezależnie od strony - może działać gdy strona jest zamknięta, obsługując push notifications czy background sync. To fundament PWA i offline-first apps.
IndexedDB to NoSQL database w przeglądarce. W przeciwieństwie do localStorage (5MB limit, tylko stringi, synchroniczne), IndexedDB przechowuje strukturalne dane bez limitu rozmiaru, obsługuje indeksy, transakcje i zapytania. Jest asynchroniczne - nie blokuje UI. Używasz go dla offline data, cache aplikacji, przechowywania plików i blobów.
Te trzy API często współpracują: Service Worker cachuje zasoby i API responses, IndexedDB przechowuje dane aplikacji, Web Worker przetwarza dane w tle. Razem tworzą architekturę aplikacji, która działa offline, jest szybka i responsywna.
Web Workers
Czym jest Web Worker?
Web Worker to skrypt JavaScript działający w osobnym wątku, niezależnie od main thread. Pozwala wykonywać ciężkie obliczenia bez blokowania interfejsu użytkownika.
// main.js
const worker = new Worker('worker.js');
// Wysyłanie danych do workera
worker.postMessage({ numbers: [1, 2, 3, 4, 5], operation: 'sum' });
// Odbieranie wyników
worker.onmessage = (event) => {
console.log('Wynik:', event.data); // 15
};
// Obsługa błędów
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// Zakończenie workera
// worker.terminate();
// worker.js
self.onmessage = (event) => {
const { numbers, operation } = event.data;
let result;
if (operation === 'sum') {
result = numbers.reduce((a, b) => a + b, 0);
} else if (operation === 'average') {
result = numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
// Wysyłanie wyniku z powrotem
self.postMessage(result);
};
Typy Web Workers
Dedicated Worker - przypisany do jednej strony:
// Jeden worker dla jednej strony
const worker = new Worker('worker.js');
Shared Worker - współdzielony między wieloma stronami/tabami:
// shared-worker.js
const connections = [];
self.onconnect = (event) => {
const port = event.ports[0];
connections.push(port);
port.onmessage = (e) => {
// Broadcast do wszystkich połączonych stron
connections.forEach(p => p.postMessage(e.data));
};
port.start();
};
// main.js
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.start();
sharedWorker.port.postMessage('Hello from tab');
sharedWorker.port.onmessage = (e) => console.log(e.data);
Worker Module (ES Modules w workerze):
// Nowoczesna składnia z modułami
const worker = new Worker('worker.js', { type: 'module' });
// worker.js
import { heavyCalculation } from './utils.js';
self.onmessage = async (event) => {
const result = await heavyCalculation(event.data);
self.postMessage(result);
};
Transferable Objects
Normalna komunikacja kopiuje dane (structured clone). Dla dużych danych użyj Transferable Objects:
// main.js
const largeBuffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
// ŹLE - kopiowanie 100MB
// worker.postMessage(largeBuffer);
// DOBRZE - transfer własności (zero-copy)
worker.postMessage(largeBuffer, [largeBuffer]);
// Po transferze, largeBuffer.byteLength === 0 w main thread!
console.log(largeBuffer.byteLength); // 0
// worker.js
self.onmessage = (event) => {
const buffer = event.data;
// Teraz worker jest właścicielem buffera
console.log(buffer.byteLength); // 104857600
// Przetwarzanie...
const result = processBuffer(buffer);
// Transfer z powrotem
self.postMessage(result, [result]);
};
Co Web Worker może, a czego nie może
// ✅ Worker MA dostęp do:
// - XMLHttpRequest / fetch
// - WebSockets
// - IndexedDB
// - setTimeout / setInterval
// - navigator (częściowo)
// - location (read-only)
// - crypto
// - performance
// - importScripts()
// ❌ Worker NIE MA dostępu do:
// - DOM (document, window)
// - localStorage / sessionStorage
// - alert / confirm / prompt
// - parent / opener
// Przykład: fetch w workerze
self.onmessage = async (event) => {
const { url } = event.data;
try {
const response = await fetch(url);
const data = await response.json();
self.postMessage({ success: true, data });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};
Praktyczny przykład: przetwarzanie obrazu
// main.js
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageWorker = new Worker('image-worker.js');
function applyFilter(filterType) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Transfer dla wydajności
imageWorker.postMessage(
{ imageData: imageData.data.buffer, width: canvas.width, height: canvas.height, filter: filterType },
[imageData.data.buffer]
);
}
imageWorker.onmessage = (event) => {
const { buffer, width, height } = event.data;
const imageData = new ImageData(new Uint8ClampedArray(buffer), width, height);
ctx.putImageData(imageData, 0, 0);
};
// image-worker.js
self.onmessage = (event) => {
const { buffer, width, height, filter } = event.data;
const data = new Uint8ClampedArray(buffer);
if (filter === 'grayscale') {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
} else if (filter === 'invert') {
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
}
self.postMessage({ buffer: data.buffer, width, height }, [data.buffer]);
};
Service Workers
Cykl życia Service Worker
┌─────────────┐
│ Registration│ → navigator.serviceWorker.register()
└──────┬──────┘
↓
┌─────────────┐
│ Installation│ → event: 'install' (pre-cache zasobów)
└──────┬──────┘
↓
┌─────────────┐
│ Waiting │ → czeka na zamknięcie starych tabów
└──────┬──────┘
↓
┌─────────────┐
│ Activation │ → event: 'activate' (czyszczenie starych cache'ów)
└──────┬──────┘
↓
┌─────────────┐
│ Running │ → event: 'fetch', 'push', 'sync'
└──────┬──────┘
↓
┌─────────────┐
│ Redundant │ → zastąpiony przez nową wersję
└─────────────┘
Rejestracja i podstawowy Service Worker
// main.js - rejestracja
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/' // Kontroluje URL-e zaczynające się od /
});
console.log('SW registered:', registration.scope);
// Sprawdzanie aktualizacji
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Nowa wersja dostępna
showUpdateNotification();
}
});
});
} catch (error) {
console.error('SW registration failed:', error);
}
});
}
// sw.js - Service Worker
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html'
];
// Instalacja - pre-cache zasobów
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting()) // Natychmiast aktywuj
);
});
// Aktywacja - czyszczenie starych cache'ów
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
))
.then(() => self.clients.claim()) // Przejmij kontrolę
);
});
// Fetch - przechwytywanie żądań
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
.catch(() => caches.match('/offline.html'))
);
});
Strategie cachowania
Cache First - dla statycznych zasobów:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => {
if (cached) {
return cached; // Zwróć z cache
}
return fetch(event.request).then(response => {
// Zapisz do cache
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
});
})
);
});
Network First - dla dynamicznych danych:
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
// Zapisz świeże dane do cache
const clone = response.clone();
caches.open(API_CACHE).then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => {
// Fallback do cache gdy offline
return caches.match(event.request);
})
);
}
});
Stale While Revalidate - szybkość + świeżość:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(cached => {
const fetched = fetch(event.request).then(response => {
cache.put(event.request, response.clone());
return response;
});
// Zwróć cache natychmiast, w tle pobierz nową wersję
return cached || fetched;
});
})
);
});
Cache z timeout na sieć:
function networkWithTimeout(request, timeout = 3000) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject(new Error('Timeout')), timeout);
fetch(request).then(response => {
clearTimeout(timeoutId);
resolve(response);
}).catch(reject);
});
}
self.addEventListener('fetch', (event) => {
event.respondWith(
networkWithTimeout(event.request, 3000)
.catch(() => caches.match(event.request))
);
});
Push Notifications
// main.js - subskrypcja
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // Wymagane - każdy push musi pokazać notification
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// Wyślij subscription do serwera
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
// sw.js - obsługa push
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? { title: 'Notification', body: 'No content' };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon-192.png',
badge: '/badge.png',
data: { url: data.url },
actions: [
{ action: 'open', title: 'Otwórz' },
{ action: 'dismiss', title: 'Zamknij' }
]
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url || '/')
);
}
});
Background Sync
// main.js - rejestracja sync
async function saveForLater(data) {
// Zapisz do IndexedDB
await saveToIndexedDB('outbox', data);
// Zarejestruj sync
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-outbox');
}
// sw.js - obsługa sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-outbox') {
event.waitUntil(syncOutbox());
}
});
async function syncOutbox() {
const items = await getFromIndexedDB('outbox');
for (const item of items) {
try {
await fetch('/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
});
await deleteFromIndexedDB('outbox', item.id);
} catch (error) {
// Zostaw w outbox, spróbuj ponownie później
throw error; // Retry sync
}
}
}
IndexedDB
Podstawy IndexedDB
IndexedDB to asynchroniczna, transakcyjna baza danych NoSQL w przeglądarce.
// Otwieranie bazy danych
const dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
// Wywoływane przy pierwszym otwarciu lub upgrade wersji
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Tworzenie object store (tabeli)
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
store.createIndex('email', 'email', { unique: true });
store.createIndex('age', 'age', { unique: false });
}
if (!db.objectStoreNames.contains('orders')) {
const orderStore = db.createObjectStore('orders', { keyPath: 'id' });
orderStore.createIndex('userId', 'userId', { unique: false });
orderStore.createIndex('date', 'date', { unique: false });
}
};
});
CRUD Operations
class IndexedDBWrapper {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open(upgradeCallback) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
upgradeCallback(event.target.result, event.oldVersion);
};
});
}
// CREATE
async add(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.add(data);
request.onsuccess = () => resolve(request.result); // Zwraca key
request.onerror = () => reject(request.error);
});
}
// READ
async get(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// READ ALL
async getAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// UPDATE
async put(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data); // put = add or update
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// DELETE
async delete(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// QUERY by index
async getByIndex(storeName, indexName, value) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.getAll(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// Użycie
const db = new IndexedDBWrapper('MyApp', 1);
await db.open((database, oldVersion) => {
if (oldVersion < 1) {
const store = database.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
store.createIndex('email', 'email', { unique: true });
}
});
// CRUD
const userId = await db.add('users', { name: 'John', email: 'john@example.com' });
const user = await db.get('users', userId);
await db.put('users', { id: userId, name: 'John Doe', email: 'john@example.com' });
await db.delete('users', userId);
Cursory i zaawansowane zapytania
// Iteracja z cursorem
async function getAllWithCursor(storeName) {
return new Promise((resolve, reject) => {
const results = [];
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue(); // Przejdź do następnego
} else {
resolve(results); // Koniec
}
};
request.onerror = () => reject(request.error);
});
}
// Range queries
async function getUsersInAgeRange(minAge, maxAge) {
return new Promise((resolve, reject) => {
const results = [];
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
// IDBKeyRange dla zakresu
const range = IDBKeyRange.bound(minAge, maxAge);
const request = index.openCursor(range);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
});
}
// Key ranges
IDBKeyRange.only(value); // Dokładna wartość
IDBKeyRange.lowerBound(x); // >= x
IDBKeyRange.lowerBound(x, true); // > x (exclusive)
IDBKeyRange.upperBound(y); // <= y
IDBKeyRange.upperBound(y, true); // < y
IDBKeyRange.bound(x, y); // x <= value <= y
IDBKeyRange.bound(x, y, true, true); // x < value < y
Transakcje
// Transakcja obejmująca wiele operacji
async function transferMoney(fromId, toId, amount) {
return new Promise((resolve, reject) => {
const transaction = db.transaction('accounts', 'readwrite');
const store = transaction.objectStore('accounts');
// Transakcja gwarantuje atomowość
transaction.oncomplete = () => resolve(true);
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(new Error('Transaction aborted'));
// Pobierz oba konta
const fromRequest = store.get(fromId);
fromRequest.onsuccess = () => {
const fromAccount = fromRequest.result;
if (fromAccount.balance < amount) {
transaction.abort(); // Przerwij transakcję
return;
}
const toRequest = store.get(toId);
toRequest.onsuccess = () => {
const toAccount = toRequest.result;
// Aktualizuj oba konta
fromAccount.balance -= amount;
toAccount.balance += amount;
store.put(fromAccount);
store.put(toAccount);
// Transakcja zostanie automatycznie zatwierdzona
};
};
});
}
Migracje schematu
const DB_VERSION = 3;
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
// Migracja od wersji 0 (nowa baza)
if (oldVersion < 1) {
const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
userStore.createIndex('email', 'email', { unique: true });
}
// Migracja od wersji 1 do 2
if (oldVersion < 2) {
const userStore = event.target.transaction.objectStore('users');
userStore.createIndex('createdAt', 'createdAt', { unique: false });
}
// Migracja od wersji 2 do 3
if (oldVersion < 3) {
// Dodaj nowy object store
const ordersStore = db.createObjectStore('orders', { keyPath: 'id' });
ordersStore.createIndex('userId', 'userId', { unique: false });
// Usuń stary index
const userStore = event.target.transaction.objectStore('users');
if (userStore.indexNames.contains('oldIndex')) {
userStore.deleteIndex('oldIndex');
}
}
};
Cache API
Cache API często używane z Service Workers, ale dostępne też w main thread:
// Otwieranie cache
const cache = await caches.open('my-cache-v1');
// Dodawanie do cache
await cache.add('/styles.css'); // Fetch + cache
await cache.addAll(['/app.js', '/index.html']); // Batch
// Ręczne dodawanie response
const response = await fetch('/api/data');
await cache.put('/api/data', response.clone());
// Pobieranie z cache
const cached = await cache.match('/styles.css');
if (cached) {
const text = await cached.text();
}
// Sprawdzanie wszystkich cache'ów
const response = await caches.match('/styles.css'); // Szuka we wszystkich
// Usuwanie
await cache.delete('/old-file.js');
await caches.delete('old-cache-v1'); // Usuń cały cache
// Lista cache'ów
const keys = await caches.keys(); // ['my-cache-v1', 'api-cache']
Porównanie storage APIs
| Cecha | localStorage | sessionStorage | IndexedDB | Cache API |
|---|---|---|---|---|
| Limit | ~5MB | ~5MB | Brak* | Brak* |
| Typy danych | String | String | Wszystkie | Response |
| Synchroniczność | Sync | Sync | Async | Async |
| Transakcje | Nie | Nie | Tak | Nie |
| Indeksy | Nie | Nie | Tak | Nie |
| Dostęp z Worker | Nie | Nie | Tak | Tak |
| Persistence | Trwałe | Sesja | Trwałe | Trwałe |
| Use case | Proste ustawienia | Dane sesji | App data | HTTP cache |
*Limit zależy od przeglądarki i dostępnego miejsca
Pytania rekrutacyjne z odpowiedziami
Pytanie 1: Kiedy użyjesz Web Worker zamiast async/await?
Odpowiedź: Async/await i Web Worker rozwiązują różne problemy.
Async/await obsługuje operacje I/O (fetch, file read) - kod "czeka" na odpowiedź, ale nie blokuje main thread, bo operacja dzieje się poza JavaScript engine.
Web Worker potrzebny jest dla CPU-intensive tasks - gdy sam JavaScript wykonuje ciężkie obliczenia. Przykłady:
- Parsowanie dużego JSON (>10MB)
- Przetwarzanie obrazów/video
- Kryptografia
- Symulacje, algorytmy grafowe
- Kompresja/dekompresja
// Async/await - I/O, nie blokuje
const data = await fetch('/api/huge-data'); // OK
// Web Worker - CPU-intensive
// Parsowanie 50MB JSON zablokuje UI na kilka sekund
const worker = new Worker('parser.js');
worker.postMessage(hugeJsonString);
Reguła: jeśli operacja trwa >50ms i jest obliczeniowa (nie I/O), użyj Worker.
Pytanie 2: Wyjaśnij różnicę między skipWaiting() a clients.claim()
Odpowiedź: Obie metody kontrolują aktywację Service Worker, ale na różnych etapach.
skipWaiting() - wywoływane w evencie install. Pomija etap "waiting" - nowy Service Worker natychmiast staje się aktywny, nie czekając aż użytkownik zamknie wszystkie taby.
clients.claim() - wywoływane w evencie activate. Sprawia, że aktywny Service Worker przejmuje kontrolę nad wszystkimi otwartymi stronami natychmiast, zamiast czekać na refresh.
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.addAll(ASSETS))
.then(() => self.skipWaiting()) // Nie czekaj
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
cleanup().then(() => self.clients.claim()) // Przejmij kontrolę
);
});
Uwaga: używanie obu może spowodować niespójności - strona załadowana starą wersją będzie obsługiwana przez nowego SW.
Pytanie 3: Jak zaimplementujesz offline-first dla API?
Odpowiedź: Strategia: zapisuj dane do IndexedDB, Service Worker decyduje o źródle.
// sw.js
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
// Zapisz do IndexedDB przez clone
const clone = response.clone();
saveToIndexedDB(event.request.url, clone);
return response;
})
.catch(async () => {
// Offline - zwróć z IndexedDB
const cached = await getFromIndexedDB(event.request.url);
if (cached) {
return new Response(JSON.stringify(cached), {
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('{"error": "Offline"}', { status: 503 });
})
);
}
});
Dla mutacji (POST/PUT/DELETE) - queue w IndexedDB, sync gdy online:
// Zapisz do outbox gdy offline
if (!navigator.onLine) {
await saveToOutbox({ method: 'POST', url, data });
await registration.sync.register('sync-outbox');
}
Pytanie 4: Czym jest structured clone i jakie ma ograniczenia?
Odpowiedź: Structured clone to algorytm kopiowania danych między kontekstami (postMessage, IndexedDB, History API). Jest głębszy niż JSON.parse/stringify.
Co obsługuje:
- Primitives (string, number, boolean, null, undefined)
- Object, Array
- Date, RegExp, Map, Set
- ArrayBuffer, TypedArray, DataView
- Blob, File, FileList
- ImageData
Czego NIE obsługuje:
- Functions
- DOM nodes
- Symbols
- Property descriptors (getters/setters)
- Prototype chain (tylko own properties)
- Error objects (częściowo)
// OK
worker.postMessage({
date: new Date(),
map: new Map([['a', 1]]),
buffer: new ArrayBuffer(10)
});
// BŁĄD - funkcje nie są klonowane
worker.postMessage({
callback: () => console.log('x') // Error!
});
// Workaround dla funkcji - przekaż nazwę
worker.postMessage({ operation: 'sum', data: [1, 2, 3] });
Pytanie 5: Jak obsłużysz aktualizację Service Worker bez przerywania sesji?
Odpowiedź: Problem: skipWaiting() aktywuje nowego SW, ale strona może mieć niespójny stan.
Rozwiązanie: powiadom użytkownika i pozwól mu zdecydować.
// main.js
navigator.serviceWorker.register('/sw.js').then(registration => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Nowa wersja czeka
showUpdateBanner();
}
});
});
});
function showUpdateBanner() {
const banner = document.getElementById('update-banner');
banner.style.display = 'block';
document.getElementById('update-btn').onclick = () => {
// Wyślij sygnał do SW by aktywował się
navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
};
}
// Reload gdy nowy SW przejmie kontrolę
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
// sw.js
self.addEventListener('message', (event) => {
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
Pytanie 6: Jakie są limity storage w przeglądarce?
Odpowiedź: Limity zależą od przeglądarki i kontekstu:
localStorage/sessionStorage: ~5-10MB per origin
IndexedDB + Cache API: współdzielony limit per origin:
- Chrome: do 80% wolnego miejsca na dysku (max ~2GB per origin)
- Firefox: do 2GB per origin
- Safari: 1GB, z pytaniem o więcej
Storage API pozwala sprawdzić i zarządzać:
// Sprawdź dostępne miejsce
const estimate = await navigator.storage.estimate();
console.log(`Used: ${estimate.usage} bytes`);
console.log(`Quota: ${estimate.quota} bytes`);
console.log(`Percent: ${(estimate.usage / estimate.quota * 100).toFixed(2)}%`);
// Poproś o persistent storage (nie zostanie wyczyszczone)
const persistent = await navigator.storage.persist();
if (persistent) {
console.log('Storage will not be cleared automatically');
}
Eviction policy: gdy brakuje miejsca, przeglądarka może usunąć dane LRU (least recently used) origin. Persistent storage jest chroniony.
Pytanie 7: Jak zaimplementujesz Web Worker pool?
Odpowiedź: Worker pool zarządza pulą workerów, dystrybuując zadania.
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
this.workers = [];
this.queue = [];
this.activeJobs = new Map();
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
worker.busy = false;
worker.onmessage = (e) => this.handleMessage(worker, e);
this.workers.push(worker);
}
}
execute(data) {
return new Promise((resolve, reject) => {
const job = { data, resolve, reject };
const worker = this.workers.find(w => !w.busy);
if (worker) {
this.runJob(worker, job);
} else {
this.queue.push(job); // Kolejkuj
}
});
}
runJob(worker, job) {
worker.busy = true;
const jobId = Math.random().toString(36);
this.activeJobs.set(jobId, { worker, job });
worker.postMessage({ jobId, data: job.data });
}
handleMessage(worker, event) {
const { jobId, result, error } = event.data;
const { job } = this.activeJobs.get(jobId);
if (error) {
job.reject(new Error(error));
} else {
job.resolve(result);
}
this.activeJobs.delete(jobId);
worker.busy = false;
// Wykonaj następne zadanie z kolejki
if (this.queue.length > 0) {
this.runJob(worker, this.queue.shift());
}
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Użycie
const pool = new WorkerPool('calculator.js', 4);
const results = await Promise.all([
pool.execute({ numbers: [1, 2, 3] }),
pool.execute({ numbers: [4, 5, 6] }),
pool.execute({ numbers: [7, 8, 9] }),
// ... więcej zadań niż workerów - kolejkowanie
]);
Pytanie 8: Jak debugować Service Worker?
Odpowiedź: Narzędzia i techniki:
1. Chrome DevTools:
- Application → Service Workers - status, lifecycle, update
- Application → Cache Storage - zawartość cache'ów
- Application → IndexedDB - zawartość bazy
- Network - oznacza żądania obsłużone przez SW
2. Bypass for network:
Application → Service Workers → Bypass for network
3. Console w SW:
// sw.js
console.log('SW: install event');
self.addEventListener('fetch', (event) => {
console.log('SW: fetch', event.request.url);
});
4. Update on reload:
Application → Service Workers → Update on reload
5. Wymuszenie aktualizacji:
// W konsoli
navigator.serviceWorker.getRegistration().then(reg => {
reg.update();
});
6. Unregister:
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(reg => reg.unregister());
});
7. chrome://serviceworker-internals - wszystkie SW w przeglądarce
Pytanie 9: Kiedy IndexedDB, a kiedy localStorage?
Odpowiedź: localStorage:
- Proste pary klucz-wartość
- Małe ilości danych (<5MB)
- Stringi (JSON.stringify dla obiektów)
- Synchroniczny dostęp OK (np. token auth przy starcie)
- Nie potrzeba indeksów/zapytań
IndexedDB:
- Złożone struktury danych
- Duże ilości danych (>5MB)
- Dowolne typy (obiekty, blobs, files)
- Potrzeba indeksów i zapytań
- Operacje asynchroniczne
- Dostęp z Web Worker / Service Worker
- Transakcyjność
// localStorage - prosty token
localStorage.setItem('token', 'abc123');
// IndexedDB - dane aplikacji offline
// - lista produktów z wyszukiwaniem
// - cache odpowiedzi API
// - kolejka operacji offline
Reguła: jeśli robisz JSON.stringify/parse i dane rosną - przejdź na IndexedDB.
Pytanie 10: Jak zaimplementujesz progress dla długiego zadania w Web Worker?
Odpowiedź: Worker raportuje postęp przez postMessage:
// main.js
const worker = new Worker('processor.js');
worker.postMessage({ items: largeArray });
worker.onmessage = (event) => {
const { type, progress, result } = event.data;
if (type === 'progress') {
updateProgressBar(progress); // 0-100
} else if (type === 'complete') {
handleResult(result);
}
};
// processor.js
self.onmessage = (event) => {
const { items } = event.data;
const results = [];
for (let i = 0; i < items.length; i++) {
results.push(processItem(items[i]));
// Raportuj postęp co 1%
if (i % Math.floor(items.length / 100) === 0) {
const progress = Math.round((i / items.length) * 100);
self.postMessage({ type: 'progress', progress });
}
}
self.postMessage({ type: 'complete', result: results });
};
Dla bardzo długich operacji - dodaj możliwość anulowania:
// main.js
const controller = new AbortController();
worker.postMessage({ items, signal: controller.signal });
document.getElementById('cancel').onclick = () => {
controller.abort();
worker.terminate();
};
Zadania praktyczne
Zadanie 1: Zaimplementuj offline-first todo list
Wymagania:
- Todo items zapisywane w IndexedDB
- Service Worker cache'uje shell aplikacji
- Dodawanie/edycja działa offline
- Synchronizacja gdy online
Rozwiązanie (szkielet)
// db.js - IndexedDB wrapper
class TodoDB {
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('TodoApp', 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true });
db.createObjectStore('outbox', { keyPath: 'id', autoIncrement: true });
};
request.onsuccess = () => { this.db = request.result; resolve(); };
});
}
async addTodo(todo) {
const id = await this.add('todos', { ...todo, synced: false });
if (!navigator.onLine) {
await this.add('outbox', { type: 'add', todoId: id, data: todo });
}
return id;
}
async syncOutbox() {
const items = await this.getAll('outbox');
for (const item of items) {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(item.data)
});
await this.delete('outbox', item.id);
}
}
}
// sw.js
const CACHE_NAME = 'todo-app-v1';
const SHELL = ['/', '/index.html', '/app.js', '/styles.css'];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(SHELL)));
});
self.addEventListener('fetch', (e) => {
if (e.request.url.includes('/api/')) {
e.respondWith(
fetch(e.request).catch(() => new Response('[]'))
);
} else {
e.respondWith(
caches.match(e.request).then(r => r || fetch(e.request))
);
}
});
self.addEventListener('sync', (e) => {
if (e.tag === 'sync-todos') {
e.waitUntil(syncTodos());
}
});
Zadanie 2: Web Worker do przetwarzania CSV
Zaimplementuj worker parsujący duży plik CSV z raportowaniem postępu.
Rozwiązanie
// csv-worker.js
self.onmessage = (event) => {
const { csvText } = event.data;
const lines = csvText.split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const results = [];
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const values = parseCSVLine(lines[i]);
const row = {};
headers.forEach((h, idx) => { row[h] = values[idx]; });
results.push(row);
// Progress co 1000 linii
if (i % 1000 === 0) {
self.postMessage({
type: 'progress',
current: i,
total: lines.length,
percent: Math.round((i / lines.length) * 100)
});
}
}
self.postMessage({ type: 'complete', data: results });
};
function parseCSVLine(line) {
const result = [];
let current = '';
let inQuotes = false;
for (const char of line) {
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
result.push(current.trim());
current = '';
} else {
current += char;
}
}
result.push(current.trim());
return result;
}
// main.js
const worker = new Worker('csv-worker.js');
const progressBar = document.getElementById('progress');
document.getElementById('file').onchange = async (e) => {
const file = e.target.files[0];
const text = await file.text();
worker.postMessage({ csvText: text });
};
worker.onmessage = (e) => {
if (e.data.type === 'progress') {
progressBar.value = e.data.percent;
progressBar.textContent = `${e.data.percent}%`;
} else if (e.data.type === 'complete') {
console.log('Parsed rows:', e.data.data.length);
renderTable(e.data.data);
}
};
Zobacz też
- Kompletny Przewodnik - Rozmowa Frontend Developer - pełny przewodnik frontend
- 15 Najtrudniejszych Pytań Rekrutacyjnych z JavaScript - asynchroniczność, event loop
- Web Performance i Core Web Vitals - optymalizacja wydajności
- Frontend Security - CSP, CORS, XSS - bezpieczeństwo
- React 19 - Nowe Hooki - Server Components, use()
Napisane przez zespół Flipcards, na podstawie doświadczeń z rozmów rekrutacyjnych w firmach technologicznych i software house'ach w Polsce i za granicą.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
