19. modulAmi Workers-en máshogy megy
Nuxt for Devs · 19. modul

Ami Workers-en máshogy megy

Ez a modul katalógus, nem elbeszélés — arra való, hogy visszalapozz hozzá, amikor valami érthetetlen történik. A meglepetések nem véletlenszerűek: hat gyökérokra vezethetők vissza, és ha a hatot érted, a hetvenedik buktatót is ki fogod találni magadtól.

19.1A hat gyökérok

① Nincs Node a futásidő nem szerver, hanem böngésző-szerű sandbox · nincs eval, nincs natív addon · a node: modulok egy része stub ② Az isolate újrahasznosul nem kérésenként indul újra, de bármikor eldobható · modul-scope = megosztott · hidegindítás bármikor jöhet ③ Az óra áll Date.now() csak I/O-nál lép, futás közben nem · Spectre-védelem · a mérés és a várakozás eltörik ④ Minden korlátos CPU, memória, méret, alhívás, kapcsolat · a limit kemény, nem lassulás · lokálisan egyik sem látszik ⑤ A tárolás elosztott nincs egy igazság-példány, van sok, késleltetve · KV: eventual consistency · cache: adatközpontonként külön ⑥ A kérés véget ér a válasz után a futás azonnal leállhat · nincs háttérfeldolgozás · waitUntil vagy semmi
Minden buktató, amivel ebben a tanfolyamban találkoztál, visszavezethető ezek valamelyikére. A 19.8-as kereszttábla ezt teszi kereshetővé.

19.2① Nincs Node — a futásidő nem szerver

A workerd nem Node, hanem egy V8-alapú, böngészőhöz közelebb álló sandbox. A nodejs_compat flag sokat pótol belőle, de nem mindent, és a „nem mindent” pontos alakja évről évre változik. A 2026-os állapot:

ÁllapotModulok
Teljesassert, async_hooks, buffer, crypto, errors, events, fs, globals, http, https, net, path, process, punycode, querystring, stream, string_decoder, timers, url, util, zlib
Részlegesconsole, dns, module, os, perf_hooks, test, tls
Stub (importálható, de nem működik)http2, vm, cluster, domain, trace_events, wasi, dgram, inspector, sqlite, child_process, readline, repl, tty, v8, worker_threads

Az egész csak nodejs_compat flaggel és 2024-09-23 vagy későbbi compatibility_date-tel él.

A stub-modulok új veszélyt hoztak, amiről kevesen beszélnek. Sok könyvtár így derít fel képességeket: try { const w = require('worker_threads'); useParallel() } catch { useSerial() }. Régen az import elhasalt, a catch ág lefutott, és a könyvtár a soros útra váltott — működött. Most az import sikerül (a stub létezik), a felderítés azt hiszi, van párhuzamosítás, és a hiba csak a tényleges hívásnál jelenik meg — mélyebben, homályosabb hibaüzenettel. Ha egy könyvtár korábban ment és egy compatibility_date-emelés után elromlott, itt keresd.

Az eval tiltása — és amit magával ránt

Biztonsági okból nem engedélyezett az eval(), a new Function, és a bufferből történő WebAssembly-fordítás. Ez nem konfigurálható.

EvalError: Code generation from strings disallowed for this context

Ez a hibaüzenet a leggyakoribb „miért nem megy ez a könyvtár?” válasza. Bárminek eltörik, ami futásidőben állít elő JavaScriptet: a JSON Schema-validátorok jelentős része (az AJV sémából kódot fordít), a futásidőben fordító sablonmotorok, néhány minifier és kifejezés-kiértékelő.

Ezért ajánlott a Zod vagy a Valibot az AJV-alapú validátorok helyett a 8. modulban: ezek nem generálnak kódot, hanem függvényeket kompozálnak. Sablonoknál a válasz a build-időben előfordítás: a legtöbb sablonmotor tud precompile módot, ami a fordítást a build-lépésbe teszi, és futásidőben csak a kész függvényt hívja.

A node:fs valósága

A táblázatban a fs zölden szerepel, és ez félrevezető lehet: nem a lemezed van mögötte, hanem egy memóriában élő virtuális fájlrendszer.

ÚtvonalMi ez
/bundlea Workered bundle-jébe csomagolt modulok, csak olvasható
/tmpírható ideiglenes könyvtár — kérésenként külön és üres
/dev/dev/null, /dev/random, /dev/zero, /dev/full

Két dolgot érdemes tudni róla. Egyrészt a /tmp tartalma nem marad meg kérések között, és nem látható más, párhuzamosan futó kérésekből — vagyis ideiglenes fájlra építő könyvtár működni fog, de „töltsük fel egyszer, aztán használjuk” cache-elés nem. Másrészt a fájlok a memórialimitedbe számítanak, fájlonként legfeljebb 128 MB, és minden művelet szinkron. A fájlidőbélyegek mindig 1970-01-01 — ami néhány könyvtárat meg tud lepni.

19.3② Az isolate újrahasznosul

Ez a 6. modul központi témája volt, itt csak a katalógus-bejegyzés. A Worker nem indul újra kérésenként: ugyanaz az isolate sok kérést szolgál ki, aztán bármikor eldobható.

KövetkezményTünet
Modul-scope állapot megosztottA felhasználó más adatait látja; „néha rossz a tenant”
Modul-scope inicializálás minden hidegindításkor futSzórványos lassú kérések; indulási limit túllépése
Nincs garancia a folytonosságraA memóriában tartott cache „véletlenszerűen” üres
Nincs process.on('exit')-szerű életciklusA „takarítsunk le leálláskor” minta sosem fut le
A legalattomosabb változat nem a saját kódodban van, hanem egy könyvtárban. Egy SDK, ami modul-szinten cache-el egy hitelesített klienst „a teljesítmény kedvéért”, Node-ban helyesen viselkedik (egy folyamat, egy felhasználói kontextus), Workers-en viszont a kérések között szivárogtat. Ha egy könyvtár init(apiKey)-t vár egyszer, és utána globálisan használható, gyanakodj: kérésenként hozd létre, vagy ellenőrizd, hogy a példány valóban állapotmentes-e.

19.4③ Az óra áll

Ez a legmeglepőbb tétel a listán, és a legtöbb fejlesztő akkor találkozik vele, amikor egy mérés következetesen nullát ad. A Cloudflare Spectre-védelmi modellje kimondja:

Date.now() a legutóbbi I/O idejét adja vissza. Kódfutás közben nem halad előre.” Vagyis két Date.now() hívás között, ha nem történt I/O, ugyanazt az értéket kapod — akármennyi számítást végeztél közben. Ez nem hiba, hanem szándékos: időzítéses oldalcsatorna-támadások ellen véd.
// ez MINDIG 0-t ír ki Workers-en
const t0 = Date.now()
for (let i = 0; i < 1e7; i++) { szamol(i) }
console.log(Date.now() - t0)   // → 0

// ez viszont VÉGTELEN CIKLUS — a feltétel sosem válik hamissá
const hatarido = Date.now() + 100
while (Date.now() < hatarido) { /* pörgő várakozás */ }

// ez viszont MŰKÖDIK: az await I/O, tehát az óra lép
await new Promise(r => setTimeout(r, 100))
Amit eltörMiértHelyette
Kódon belüli időmérésa különbség mindig 0a 16. modul CPU-profilja a DevToolsban
Pörgő várakozás (while (Date.now() < x))végtelen ciklus → CPU-limitawait new Promise(r => setTimeout(r, ms))
Szinkron backoff-hurok újrapróbálkozásbanugyanaz a végtelen ciklusawait-es backoff
Szinkron blokkon belüli rate limiterminden esemény ugyanazt az időbélyeget kapjaDurable Object vagy a Rate Limiting binding
Egyediségre használt időbélyegegy kérésen belül ütközikcrypto.randomUUID()
Nuxt-oldali vetület: a 6. modulban a hidratálási eltérés első okaként a new Date()-et neveztük meg — a szerver és a kliens más időt lát. Most kapott egy második indokot is: a szerveren az idő nem is halad a renderelés alatt, tehát még a szerveren belüli két hívás sem ad feltétlenül különböző értéket. Az időt vagy fentről add be propként, vagy kliensoldalon számold — de ne várd, hogy az SSR-renderelés „mérje” magát.

19.5④ Minden korlátos

A limitek nem lassulást jelentenek, hanem kemény falat: a kérés elhasal. Egy helyen, hivatkozásként:

ErőforrásFreePaidTúllépéskor
Worker-méret (gzip)3 MB10 MBa deploy elutasítva
Worker-méret (tömörítetlen)64 MBa deploy elutasítva
Indulási idő1 másodperca Worker el sem indul
CPU-idő / kérés10 ms30 s (5 percig emelhető)a kérés megszakítva
Memória / isolate128 MBOOM, az isolate eldobva
Alhívás (subrequest)501 000a további fetch-ek elhasalnak
Egyidejű kimenő kapcsolat6a többi vár, amíg felszabadul
Statikus fájlok száma20 000100 000a deploy elutasítva
Statikus fájl mérete25 MiBa deploy elutasítva
A 6 egyidejű kimenő kapcsolat a legkevésbé ismert a listán, és egy SSR-oldalon könnyű beleszaladni. Ha egy oldal renderelése tíz külső API-t hív Promise.all-lal, azok nem futnak igazán párhuzamosan: hatosával haladnak. Az 5. modul „párhuzamosíts” tanácsa tehát hatig igaz, utána sorbaállás van. Ha tíz forrásból kell adat egy oldalhoz, az nem hangolási kérdés, hanem architekturális jelzés: kell egy aggregáló végpont vagy egy cache-réteg.

19.6⑤ A tárolás elosztott

TárolóAmit feltételeznélAmi valójában van
KVírás után azonnal olvashatóeventual consistency — az írás globális terjedése időbe telik
Cache APIglobális cacheadatközpontonként külön; a cache.delete csak a helyi PoP-ot üríti
D1bárhonnan egyformavan elsődleges régió; a távoli írások lassabbak
R2fájlrendszerobjektumtár — nincs átnevezés, nincs részleges írás
Durable Objectskálázódik, mint a többiobjektumonként egyetlen példány, sorosított hozzáférés

A Cache API három csendes szabálya

Ezek nem hibáznak, hanem szó nélkül nem cache-elnek, ami sokkal nehezebben észrevehető:

És a legfontosabb, ami a 17. modulra üt vissza: a Cache API nem működik .workers.dev domaineken. A preview URL-ek pedig pontosan ilyenek. Vagyis a cache-viselkedésedet a preview URL-en nem tudod validálni — ott másképp fog viselkedni, mint az egyedi domaineden. Ha a cache-stratégia a kérdés, azt csak custom domainen, éles vagy staging környezetben tudod megnézni.

19.7⑥ A kérés véget ér

Node-ban megszokott, hogy a válasz elküldése után a folyamat még él, és csinálhatsz utómunkát. Workers-en a futás a válasz után bármikor leállhat — hacsak nem jelzed, hogy még kell idő.

export default defineEventHandler(async (event) => {
  const rendeles = await rendelestLetrehoz(event)

  // ROSSZ: a válasz után ez talán lefut, talán nem
  analitikaKuld(rendeles)

  // JÓ: megkéred a futásidőt, hogy várja meg
  event.context.cloudflare.context.waitUntil(analitikaKuld(rendeles))

  return rendeles
})
Minta Node-bólWorkers-en
„tűz-és-felejtsd” hívás a válasz utánwaitUntil nélkül elveszhet
setInterval ütemezésrenem tartja életben a Workert → Cron Trigger
hosszú háttérfeldolgozás a kérésbenCPU-limit → Queues vagy Workflows
memóriabeli munkasoraz isolate eldobásával elvész → Queues
kapcsolat nyitva tartása pollinghozDurable Object + WebSocket
A waitUntil nem varázsszó: a benne futó munkára is vonatkozik a CPU-limit, és nem alkalmas percekig tartó feldolgozásra. A helyes gondolkodás: waitUntil az ezredmásodperces utómunkára (analitika, log, cache-melegítés), Queues mindenre, ami ennél komolyabb.

19.8Tünet → gyökérok kereszttábla

Ez a modul lényegi haszna: ha valami furcsát látsz, itt keresd meg.

TünetGyökérokHol olvass róla
Néha más felhasználó adatát látom② isolate6. modul
„Néha lassú”, szórványosan② hidegindítás15. modul
Az időmérésem 0-t ad③ óra19.4
A kérés megszakad, nincs hibaüzenet③ pörgő várakozás → ④ CPU19.4 + 19.5
EvalError: Code generation…① nincs eval19.2
Egy könyvtár egy dátumemelés óta romlott el① stub-modul19.2
Deploy: „Script too large”④ bundle-limit15. modul
Deploy: „startup exceeded”② + ④ indulási keret15. modul
Írok a KV-be, visszaolvasva régi érték⑤ eventual consistency18.10
A cache lokálisan megy, preview URL-en nem⑤ workers.dev korlát19.6
A cache-elésem „egyszer csak abbahagyta”Set-Cookie19.6
Az analitikám hiányos⑥ nincs waitUntil19.7
10 párhuzamos fetch lassabb, mint várnám④ 6 kapcsolat19.5
Az ideiglenes fájlom eltűnt/tmp kérésenkénti19.2
Egy oldalon nem fut a middlewareelőrenderelt → asset17.2
Chunk-404 deploy után/közbenglobális deploy / verziószórás16.9 + 17.5

19.9Könyvtárak, amik elbuknak — és mit használj helyettük

Nem megyMiértHelyette
AJV és a rá épülő validátoroksémából kódot fordít → EvalErrorZod, Valibot (8. modul)
natív bcryptC++ addonnode:crypto scrypt, vagy bcryptjs (10. modul)
sharp és a képfeldolgozóknatív binárisCloudflare Image Transformations (13. modul)
futásidőben fordító sablonmotoroknew Functionelőfordítás build-időben
worker_threads-re épülő párhuzamosításstub — importálható, nem működikQueues, vagy soros feldolgozás
fájlba író loggerek (Winston-transzportok)nincs valódi lemezstrukturált console.log (16. modul)
node-cron és setInterval-ütemezőka Worker nem él a kérések közöttCron Triggers
Puppeteer/Playwright közvetlenülnincs helyi böngésző-folyamatBrowser Rendering binding
Redis-kliensek nyers TCP-velnincs net-szintű socket a megszokott módonKV, Durable Object, vagy HTTP-s Redis
A gyors előszűrés egy új függőség előtt (13. modul): keresd a package.json-jában a "engines": { "node": … }-t és a binding.gyp-ot, nézd meg, van-e "browser" vagy "workerd" exportfeltétele, és grep-elj rá az eval, new Function, child_process, fs.watch kifejezésekre. Öt perc, és megspórol egy fél napot.

19.10Amit cserébe kapsz

Igazságtalan lenne kilenc szakasznyi korláttal zárni, mert a mérleg másik oldala is valós — és a döntés, amit az 1. modulban meghoztál, ezekért történt:

Az őszinte összegzés: a Workers akkor jó választás, ha a fenti listát előnynek tudod olvasni. Ha az alkalmazásod természete hosszú, CPU-igényes, állapotos feldolgozás — videókódolás, nagy riportgenerálás, ML-inferencia saját modellel —, akkor a korlátok nem fegyelmezni fognak, hanem akadályozni. Ilyenkor a helyes válasz nem az erőltetés, hanem a 9. modul hibrid útja: a Nuxt megy Workers-re, a nehéz munka marad ott, ahol való.

19.11Ellenőrizd magad

  1. Miért ír ki nullát ez a kód Workers-en? const t0 = Date.now(); nagySzamitas(); console.log(Date.now() - t0)
    ▸ Válasz

    Mert a Date.now() a legutóbbi I/O idejét adja vissza, és kódfutás közben nem halad előre — ez szándékos Spectre-védelem. I/O nélkül a két hívás ugyanazt az értéket adja.

  2. Mi történik ezzel: const t = Date.now() + 100; while (Date.now() < t) {}?
    ▸ Válasz

    Végtelen ciklus, mert az óra nem lép előre a szinkron futás alatt — a feltétel sosem válik hamissá. A kérés a CPU-limit túllépésével szakad meg. Helyette: await new Promise(r => setTimeout(r, 100)), mert az await I/O, tehát az óra közben lép.

  3. Egy könyvtár eddig működött, majd megemelted a compatibility_date-et, és elromlott. Mi a valószínű ok?
    ▸ Válasz

    Egy korábban hiányzó node: modul stubként elérhetővé vált. A könyvtár képesség-felderítése (try { require('worker_threads') } catch {}) most sikeresnek látja az importot, és a párhuzamos ágra megy, ami a stubon elhasal — mélyebben és homályosabb hibával, mint korábban a tiszta catch.

  4. Írsz egy fájlt a /tmp-be az első kérésben, és a másodikban olvasnád. Mi lesz?
    ▸ Válasz

    Nem lesz ott. A /tmp tartalma kérésenként külön, nem marad meg kérések között, és párhuzamos kérésekből sem látszik. Az fs Workers-en egy memóriabeli virtuális fájlrendszer, nem a lemezed.

  5. Az oldalad tíz külső API-t hív Promise.all-lal. Miért nem tízszer gyorsabb, mint sorosan?
    ▸ Válasz

    Mert egyszerre legfeljebb 6 kimenő kapcsolat lehet nyitva; a többi vár, amíg felszabadul egy. Tíz forrás egy oldalhoz architekturális jelzés: kell egy aggregáló végpont vagy cache-réteg.

  6. A cache-elésed lokálisan és élesben is működött, aztán „egyszer csak abbahagyta”. Mi a legvalószínűbb ok egy Nuxt-appban?
    ▸ Válasz

    Valami Set-Cookie fejlécet tett a válaszra — tipikusan a session-middleware. A Cache API soha nem cache-el Set-Cookie-t tartalmazó választ, és ezt csendben teszi, hibaüzenet nélkül.

  7. Miért nem tudod a cache-stratégiádat preview URL-en validálni?
    ▸ Válasz

    Mert a Cache API nem működik .workers.dev domaineken, a preview URL-ek pedig pontosan ilyenek. A cache-viselkedést csak custom domainen (staging vagy éles) tudod megnézni.

  8. Melyik két Cloudflare-eszköz váltja ki a setInterval-alapú ütemezőt és a hosszú háttérfeldolgozást?
    ▸ Válasz

    Cron Triggers az ütemezést, Queues (nagyobb munkára Workflows) a háttérfeldolgozást. A waitUntil csak ezredmásodperces utómunkára való — analitika, log, cache-melegítés —, mert rá is vonatkozik a CPU-limit.

  9. Egy SDK azt kéri, hogy egyszer hívd meg az init(apiKey)-t, aztán globálisan használható. Mi a gond?
    ▸ Válasz

    A modul-szintű, hitelesített példány az isolate-újrahasználat miatt kérések között megosztott lesz. Node-ban ez helyes (egy folyamat, egy kontextus), Workers-en szivárgás. Hozd létre kérésenként, vagy győződj meg róla, hogy a példány valóban állapotmentes.

  10. Mondj két olyan korlátot, ami valójában jó architektúrára kényszerít.
    ▸ Válasz

    Bármelyik kettő: a CPU-limit kizárja a szinkron blokkoló kódot; a bundle-limit fékezi a függőségfa hízását; az isolate-modell megakadályozza a globális állapotra építést; a subrequest-limit korán jelzi, ha egy oldal túl sok forrásból dolgozik. Mind jó gyakorlat Node-ban is — csak ott semmi nem kényszerít rájuk.

Előző18. modul — Lokális fejlesztés valódi bindingokkal Következő 20. modul — Éles üzem: SEO, i18n, cache, hibakövetés