40+ Browser APIs Pytania Rekrutacyjne 2026: Web Workers, Service Workers i IndexedDB

Sławomir Plamowski 27 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.


Browser APIs Podstawy Pytania

Jak odpowiedzieć na pytanie o Browser APIs 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."

Jak szczegółowo wyjaśnić Browser APIs na rozmowie?

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 Pytania

Web Workers to fundament wydajnych aplikacji JavaScript. Pozwalają wykonywać ciężkie obliczenia w tle bez blokowania UI.

Czym jest Web Worker i jak działa?

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);
};

Jakie są typy Web Workers?

Przeglądarka oferuje kilka typów Web Workers, które różnią się zasięgiem i sposobem komunikacji. Wybór odpowiedniego typu zależy od tego, czy worker ma być prywatny dla jednej strony, czy współdzielony między wieloma tabami.

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);
};

Czym są Transferable Objects i kiedy ich używać?

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 robić?

Web Worker działa w izolowanym kontekście, co oznacza ograniczony dostęp do niektórych API przeglądarki. Najważniejsze ograniczenie to brak dostępu do DOM - worker nie może bezpośrednio manipulować elementami strony. Ma jednak dostęp do większości Web APIs potrzebnych do przetwarzania danych.

// ✅ 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 });
  }
};

Jak użyć Web Worker do przetwarzania obrazu?

Przetwarzanie obrazów to klasyczny przypadek użycia Web Workers. Operacje na pikselach (filtry, transformacje) są CPU-intensive i mogą blokować UI na sekundy. Przenosząc je do workera, zachowujesz responsywność aplikacji. Kluczem jest użycie Transferable Objects do wydajnego przekazywania danych 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 Pytania

Service Workers działają jako programowalne proxy między aplikacją a siecią. Umożliwiają offline mode, push notifications i background sync.

Jak wygląda cykl życia Service Worker?

Service Worker przechodzi przez kilka etapów od rejestracji do aktywacji. Zrozumienie tego cyklu jest kluczowe dla prawidłowej obsługi aktualizacji i cachowania. Każdy etap oferuje odpowiednie eventy, które pozwalają wykonać niezbędne operacje.

┌─────────────┐
│ 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ę
└─────────────┘

Jak zarejestrować Service Worker?

Rejestracja Service Worker wymaga sprawdzenia wsparcia przeglądarki i obsługi potencjalnych błędów. Po pomyślnej rejestracji możesz nasłuchiwać na aktualizacje i zarządzać cache'ami. Poniższy przykład pokazuje pełny wzorzec rejestracji z obsługą aktualizacji.

// 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'))
  );
});

Jakie są strategie cachowania w Service Workers?

Wybór strategii cachowania zależy od typu zasobu i wymagań aplikacji. Statyczne zasoby (CSS, JS, obrazy) powinny być serwowane z cache'a dla szybkości, podczas gdy dane API często wymagają świeżości. Poniżej przedstawiam główne strategie wraz z ich zastosowaniami.

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))
  );
});

Jak zaimplementować Push Notifications?

Push Notifications pozwalają na wysyłanie powiadomień do użytkownika nawet gdy strona jest zamknięta. Implementacja wymaga konfiguracji po stronie klienta (subskrypcja) i serwera (wysyłanie). Kluczowe jest użycie kluczy VAPID do autoryzacji i prawidłowa obsługa eventów push w Service Worker.

// 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 || '/')
    );
  }
});

Czym jest Background Sync i jak go użyć?

Background Sync pozwala na opóźnione wykonanie operacji sieciowych - gdy użytkownik jest offline, dane są zapisywane lokalnie i automatycznie synchronizowane gdy połączenie wróci. Jest to idealne rozwiązanie dla formularzy, komentarzy czy innych operacji, które nie powinny być utracone z powodu braku sieci.

// 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 Pytania

IndexedDB to asynchroniczna, transakcyjna baza danych NoSQL w przeglądarce - idealna dla dużych ilości strukturalnych danych i aplikacji offline-first.

Czym jest IndexedDB i jak działa?

IndexedDB to asynchroniczna, transakcyjna baza danych NoSQL wbudowana w przeglądarkę. W przeciwieństwie do localStorage, przechowuje strukturalne dane bez limitu rozmiaru i obsługuje indeksy oraz złożone zapytania. Otwieranie bazy wymaga obsługi wersjonowania - event onupgradeneeded pozwala na tworzenie i modyfikację schematu.

// 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 });
    }
  };
});

Jak wykonać operacje CRUD w IndexedDB?

Operacje na IndexedDB są asynchroniczne i oparte na transakcjach. Każda operacja wymaga otwarcia transakcji z odpowiednim trybem (readonly lub readwrite) i uzyskania referencji do object store. Poniższa klasa wrapper upraszcza te operacje, opakowując callback-based API w Promise.

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);

Jak używać cursorów i zaawansowanych zapytań?

Cursory pozwalają na iterację po dużych zbiorach danych bez ładowania wszystkiego do pamięci. Są szczególnie przydatne gdy potrzebujesz przetwarzać rekordy jeden po drugim lub filtrować po zakresach wartości. IDBKeyRange umożliwia tworzenie zapytań zakresowych - od prostych porównań po złożone przedziały.

// 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

Jak działają transakcje w IndexedDB?

Transakcje w IndexedDB gwarantują atomowość operacji - albo wszystkie zmiany zostaną zapisane, albo żadna. Jest to kluczowe przy operacjach wymagających spójności danych, jak transfer środków między kontami. Transakcja jest automatycznie zatwierdzana po wykonaniu wszystkich operacji, chyba że wywołasz abort().

// 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
      };
    };
  });
}

Jak obsługiwać migracje schematu w IndexedDB?

Migracje schematu są obsługiwane przez wersjonowanie bazy danych. Gdy otwierasz bazę z wyższą wersją niż istniejąca, wywoływany jest event onupgradeneeded. W handlerze sprawdzasz poprzednią wersję i wykonujesz odpowiednie migracje - tworzenie nowych store'ów, dodawanie indeksów czy usuwanie przestarzałych struktur.

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 Pytania

Cache API jest często używane z Service Workers, ale dostępne też w main thread. Służy do przechowywania par request/response dla offline caching.

Jak używać Cache API do cachowania zasobów?

Cache API umożliwia przechowywanie par request/response dla offline caching. Jest często używane z Service Workers, ale dostępne też w main thread. API jest proste - otwierasz nazwany cache, dodajesz zasoby przez add() lub put(), i pobierasz przez match(). Możesz zarządzać wieloma cache'ami dla różnych wersji aplikacji.

// 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']

Storage APIs Pytania

Wybór odpowiedniego storage API zależy od wymagań aplikacji - rozmiaru danych, złożoności i dostępu z workerów.

Jaka jest różnica między localStorage, sessionStorage, IndexedDB i Cache API?

Przeglądarka oferuje kilka mechanizmów przechowywania danych, każdy z innymi charakterystykami. localStorage i sessionStorage są proste ale ograniczone do stringów i 5MB. IndexedDB to pełna baza danych dla złożonych struktur. Cache API jest specjalizowane dla HTTP responses. Wybór zależy od typu danych, rozmiaru i wymagań dostępu.

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

Zaawansowane Browser APIs Pytania

Ta sekcja zawiera najczęściej zadawane pytania rekrutacyjne z Browser APIs z gotowymi odpowiedziami.

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.

Jaka jest różnica 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.

Jak zaimplementować 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');
}

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] });

Jak obsłużyć 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();
  }
});

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.

Jak zaimplementować 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
]);

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

Kiedy używać 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.

Jak zaimplementować 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 Pytania

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);
  }
};

Powiązane Artykuły

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.