Browser APIs - Web Workers, Service Workers, IndexedDB - Pytania Rekrutacyjne 2026

Sławomir Plamowski 23 min czytania
frontend indexeddb javascript pwa pytania-rekrutacyjne rozmowa-rekrutacyjna service-workers web-workers

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ż


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.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.