İçeriğe atla
Ayhan Sipahi Ayhan Sipahi

CDK TypeScript Lambda: Bundler ve cdk synth Runner Seçimi

CDK TypeScript Lambda için 9 bundler ve 3 cdk synth runner'ının ölçümlü karşılaştırması; her katman için varsayılan ve onu seçtiren kural.

Bir CDK TypeScript Lambda projesi tek bir tooling kararı gibi görünür ama aslında üçtür: handler’ı Lambda’da çalıştıran runtime, her handler’ı paketleyen bundler ve cdk synth’i çalıştıran runner. Bu katmanlar gevşek bağlıdır, dolayısıyla her birinin kazananı farklıdır. Yeşil alan bir projede varsayılanlar stock nodejs22.x, NodejsFunction üzerinden esbuild ve synth için tsx’tir; üçünün arkasındaki kural şudur: bir katman prod runtime’a ne kadar yakınsa aracı o kadar sıkıcı olmalı. Sekiz gerçekçi handler üzerinde ölçülen dokuz bundler ve üç synth runner, her varsayılanı gerekçelendiriyor ve nerede override edeceğinizi işaretliyor. Sayılar tek bir Linux arm64 sandbox’tan geliyor (Node v22.22.3, aws-cdk-lib 2.261.0, iki-üç koşunun medyanı), o yüzden oranlar taşınabilir, mutlak milisaniyeler değil.

Karar tablosu

Bu benchmark, bu Rust araçlarını katmana göre sıralayıp ölçümü açık bırakan önceki toolchain haritasının ölçülmüş devamıdır. Projeyi üç bağımsız soru olarak okumak, araçların birbiriyle yarışmasını durduran şeydir:

KatmanSoruYeşil alan varsayılanıNub’un yeri
1. RuntimeLambda’da kodu ne çalıştırır?stock nodejs22.xhiçbir yerde; nub runtime değil
2. BundlingHer handler’ı ne paketler?NodejsFunction + mainFields ile esbuildbundler’ı yok; en fazla dispatch (nubx esbuild)
3. Synth runnercdk synth’i ne çalıştırır?tsxCDK’ya girdiği kapı; asıl evi dev loop

Bu seçimleri sıralayan kural, prod’a olan uzaklıktır. Runtime stock Node’da sabitlenir çünkü dokuz bundler çıktısının her biri (bun build --target node dahil) orada byte-doğrulanarak çalıştı; sabit runtime, bundler özgürlüğünün ön koşuludur. Build-time istediğin kadar agresif Rust aracı çalıştırabilir çünkü risk build’in içinde kalır ve deploy’dan önce smoke-test edilebilir; bundler değiştirmenin runtime’a hiç dokunmamasının sebebi tam budur. Dev-time ise serbest yenilik katmanıdır: bu araçlar prod’a hiç çıkmaz, o yüzden DX kazancı neredeyse bedavadır. Bu ayrımı reddeden tek araç Bun’dır. İddiası “her katmanda tek araç”tır ve build hızını almak, runtime’ını da almak demektir; bu bir gözden kaçırma değil, bilinçli bir takas ve aşağıdaki her şey onu öyle ele alıyor.

Katman 1: runtime’ı sıkıcı tut

Buradaki tüm bundler çıktıları stock nodejs22.x üzerinde çalıştı; bir require() artı gerçek bir event invoke’uyla doğrulandı, custom runtime veya layer gerekmedi. bun build --target node bile içinde Bun.* referansı olmayan saf JavaScript üretti, yani o da Node’da çalışıyor. Bu tekdüzelik, diğer iki katmanda deney yapmayı güvenli kılan şeydir: build-time’da seçtiğin hiçbir şey AWS’nin fiilen çalıştırdığını değiştirmez. Bun’ı bir runtime olarak, custom runtime layer veya OCI imajı üzerinden çalıştırmak mümkün ama buranın kapsamı dışında; aşağıdaki cold-start sayıları da bunu nadiren gerekçelendiriyor. Runtime’ı sabitlemek muhafazakârlık değildir; her yerde hızlı hareket etmeni sağlayan ön koşuldur.

Katman 2: boyutu bundler değil modül çözümleme belirler

En büyük boyut kaldıracı, hangi bundler’ı seçtiğin değil, AWS SDK’yı nasıl çözdüğüdür. Varsayılan esbuild (platform=node) AWS SDK v3’ü dist-cjs’ten çözer ve CommonJS tree-shake edilemez, o yüzden hiçbir yerden çağrılmasa bile modüller komple bundle’a girer (örneğin bowser, yaklaşık 36,7 KB). --main-fields=module,main ile ESM build’i olan dist-es’e zorlayınca tree-shaking devreye girer: toplam 3.136 KB’den 2.357 KB’ye düştü (yaklaşık yüzde 25), bowser tamamen elendi ve @smithy/core kabaca 35 KB hafifledi. Aynı bundler, tek bir bayrak.

CDK bu bayrağı doğrudan NodejsFunction üzerinde sunar:

new NodejsFunction(this, 'OrdersHandler', {
  runtime: Runtime.NODEJS_22_X,
  entry: 'lambda/handlers/orders.ts',
  bundling: {
    format: OutputFormat.CJS,        // CDK varsayılanı; açık bırakıldı
    mainFields: ['module', 'main'],  // AWS SDK'yı dist-es'ten çöz -> daha küçük
    minify: true,
  },
});

Uyarı şudur: mainFields sadece SDK’yı değil, tüm paketleri etkiler. Dual-package kütüphaneler giriş noktasını yanlış çözebilir, o yüzden varsaymak yerine çıktıyı smoke-test edin. Bir bundle’ın içine gerçekte ne girdiğini görmek için sourcemap değil, esbuild’in --metafile’ını okuyun (paket başına bytesInOutput raporlar):

node_modules/.bin/esbuild lambda/handlers/orders.ts --bundle --platform=node \
  --target=node22 --minify --main-fields=module,main \
  --outfile=dist/orders.js --metafile=meta.json

Çözümleme sabitlendikten sonra, aynı sekiz handler ve aynı standart üzerinde (handler başına self-contained CJS, node22, minify, sourcemap kapalı, tüm bağımlılıklar dahil, paylaşılan chunk yok) tüm saha:

BundlerBuild (medyan)ToplamGzipDeğerlendirme
bun build 1.3.14253 ms3.148 KB906 KBen hızlı build; ~90 MB binary ister
esbuild 0.25 (varsayılan)702 ms3.136 KB918 KBCDK varsayılanı; bayrakla küçülür
esbuild + mainFields~700 ms2.357 KB709 KBönerilen; bedavaya -%25
rspack 2.1.31.150 ms2.170 KB630 KBen iyi boyut/hız dengesi
tsup 8.5.11.151 ms2.385 KB711 KBesbuild sarmalayıcı; ESM çözümleme default
rolldown 1.1.51.853 ms2.993 KB859 KBnode-CJS’te bugün olgun değil
tsdown 0.22.42.648 ms2.993 KB859 KBrolldown ile byte-identical
webpack 5.10811.567 ms2.570 KB707 KBrspack varken gereksiz
rollup 4.62 + terser~92.000 ms2.118 KB610 KBen küçük, en yavaş (n=1)
parcel 2.16.4başarısızburaya yapısal olarak uygun değil

Aynı bağlantı dersi bir kat aşağıda, minifier’da tekrar eder. Aynı 1,2 MB’lik minify edilmemiş bundle üzerinde bağımsız minify geçişleri, hangi minifier koşarsa koşsun 653-674 KB aralığında kaldı (oxc-minify 33 ms, esbuild 64 ms, terser yaklaşık 2 s); aynı kodun esbuild ile entegre bundle+minify çıktısı ise 479 KB’ydi — sonradan koşulan her geçişten küçük. Bundle sırasında minifier cross-module scope’u hâlâ görür ve top-level isimleri güvenle kısaltabilir; sonradan gelen geçiş bunlara dokunamaz. mainFields’te olduğu gibi boyut kolu hangi aracı seçtiğin değil, aracın pipeline’da nerede durduğudur: bundle anında minify etmek, sonradan eklenen daha iyi bir minifier’ı yener.

Varsayılandan çıkan dallar, adı konmuş koşullara bağlanır:

Evet

Hayır, webpack var

Hayır, Bun runtime

Hayır

Evet

Uç durum, CI bol

Lambda bundler seç

Yeşil alan CDK projesi?

NodejsFunction üzerinden esbuild + mainFields

rspack, config-uyumlu

bun build

Ölçekte cold start kritik mi?

esbuild varsayılanını koru

rollup + terser, en küçük

Her öneri bir takas taşır. esbuild artı mainFields en ucuz yoldur ve tsup’un düz esbuild’den küçük çıkmasının sebebidir (tsup varsayılan olarak ESM çözer), ama bayrak global olduğundan dual-package kütüphaneler smoke-test ister. rspack en iyi boyut/hız dengesidir ve webpack-config uyumludur, bu da onu webpack’te olan ekipler için drop-in yapar, ama yeşil alan projenin ihtiyaç duymadığı bir config katmanı ekler. rollup artı terser en küçük artefaktı üretti ama esbuild’in build süresinin kabaca 80 ila 130 katını aldı; o ~92 saniyelik değer tek ve parçalı bir koşu, o yüzden onu kesin değil yönsel okuyun ve yalnızca CI süresi bolken ve fonksiyon sayısı çok yüksekken buna uzanın. bun build 253 ms ile en hızlıydı ama yaklaşık 90 MB’lık bir binary bağımlılığı ve daha büyük bir çıktı taşır, o yüzden ancak zaten Bun çalıştırıyorsanız kazandırır. rolldown ve tsdown aynı motoru paylaşır ve byte-identical çıktı; ikisi de bu benchmark itibarıyla self-contained node-CJS hedefine henüz ayarlanmamış, bu bir kusur değil olgunluk notudur. parcel beş bağımsız engele karşı komple başarısız oldu (bozuk scope hoisting, zod’un yanlış exclude edilmesi, dinamik import’lara zorunlu ayrı chunk’lar, custom-target çözümleme ve bir uuid exports-map hatası); bu bir ayar sorunu değil, handler başına Lambda bundling’e yapısal bir uyumsuzluktur.

Katman 3: cdk synth’i kim çalıştırır

Üç synth runner’ının hepsi byte-identical CloudFormation üretir; bu varsayılmadı, doğrulandı. Template çıktısı sabit olduğundan önemli tek eksen aws-cdk-lib grafı altındaki hızdır:

RunnerHam TS başlangıçCDK app yükücdk synth
tsx254 ms777 ms~1,7 s
nub 0.4.8112 ms1.128 ms~2,2 s
ts-node864 ms6.975 ms~9,6 s

Çıplak bir dosyada nub en hızlı başlar, tsx’in yaklaşık 2,3 katı. Ama aws-cdk-lib’in binlerce dosyası yüklendiğinde nub’un loader hook’ları maliyet ekler ve tsx yaklaşık yüzde 25 öne geçer. Buradan iki pratik çıkarım gelir. Birincisi, cdk synth için varsayılanın tsx olsun. İkincisi, hâlâ ts-node’daysan tsx ya da nub 5 ila 8 kat kazançtır, o yüzden geç. Bundling koşuya hâkim olduğunda (sekiz fonksiyon bundling ile synth edilirken) runner farkı küçülür: tsx yaklaşık 4,6 s’ye karşı nub yaklaşık 5,4 s.

Nub’un asıl evi synth değil, dev loop’tur. Orada TypeScript’i 112 ms’de başlatır ve .env yükleme, tsconfig paths, Node sürüm sabitleme ve phantom-dependency uyarısını tek araçta toplar. build komutu yok, o yüzden onu bundling yoluna sokmanın tek gerçekçi yolu dispatch’tir: Code.fromAsset local bundling içinde npx esbuild yerine nub exec esbuild (nubx) çağırmak. Çağrı başına dispatch overhead’i doğrudan binary yaklaşık 0, nubx artı 78 ms ve npx artı 167 ms’ydi. Yani nubx npx’e göre dispatch vergisini kabaca yarıya indirir ama binary’yi doğrudan çağırmak yine de en hızlısıdır; nubx yerini ham hızdan değil, lockfile-bilinçli çözümleme ve bir osv.dev zararlı-paket kontrolüyle kazanır.

Cold start bundle’la ilgili, bundler’la değil

Bir bundle sevk ettiğinde runner prod’a hiç ulaşmaz, ve cold-start init’i bundle’ın parse-ve-init maliyeti belirler. Lambda init fazını her bundle’ı stock Node 22 ile require() ederek proksilemek şunu verir:

Bundle kaynağıBoyutInit (medyan)
esbuild, küçük handler60 KB2,5 ms
rollup, s3-report389 KB26 ms
rspack, s3-report397 KB27 ms
esbuild + mainFields, s3-report479 KB39 ms
tsup, s3-report485 KB39 ms
rolldown, s3-report577 KB48 ms
esbuild varsayılan, s3-report622 KB54 ms
bun, s3-report639 KB51 ms
bundle’sız (deps node_modules’tan)141 ms

Buradan üç şey çıkar. Init burada boyutla neredeyse lineer ölçeklenir, kabaca 60 ila 85 mikrosaniye/KB, o yüzden bundler seçimi cold start’ı doğrudan oynatır: rollup ve rspack çıktısı, varsayılan esbuild’den fonksiyon başına yaklaşık 27 ms daha hızlı açılır. Ama en büyük tek kazanç bundle etmenin kendisidir: aynı bağımlılıkları node_modules’tan yüklemek 141 ms’de init olurken bundle’lı hali 26 ila 54 ms’dir, yani “klasörü zip’le at” yaklaşımı cold start’ta 3 ila 5 kat kaybettirir, ve çoğu ekibin atladığı kaldıraç budur. Son olarak, en hızlı bundler en hızlı cold boot değildir: bun 253 ms’de bundle eder ama 639 KB’lık çıktısı yaklaşık 51 ms ile en yavaş açılanlardandır. Build hızı derleyenin işine yarar; çıktı boyutu kullanıcının işine yarar.

Uzun ömürlü bir container’da tablo tersine döner. ECS, Fargate ya da EC2’de runner her cold boot’ta gerçekten çalışır ve vergi gerçektir:

BaşlatmaJS dosyasıTS dosyası
node26 ms
bun60 ms62 ms
nub130 ms112 ms
tsx227 ms254 ms
ts-node864 ms

Node’un TS sütunu boş çünkü o yol ölçülmedi; güncel Node 22 erasable-syntax TypeScript’i doğrudan çalıştırıyor, type stripping 22.18’den beri varsayılan olarak açık. Bun’ın runtime iddiası bu bağlamda gerçektir, TypeScript’i 62 ms’de başlatır, ama bu stock Node değil Bun’ın runtime’ıdır ve API-uyumluluk riski onunla gelir. Sıkıcı cevap yine kazanır: build-time’da transpile ya da bundle et ve node dist/ ile başlat, bu runner vergisini tamamen sıfırlar. Bir runner’ın yeri dev loop ve synth’tir, prod container’ı asla değil.

Sık yapılan hatalar

Ölçüm sırasında dört başarısızlık modu ortaya çıktı ve her biri tek seferlik değil, evrensel bir tuzak.

Runner değişince modül semantiği değişebilir. Aynı .ts dosyası verildiğinde nub onu ESM, tsx CJS olarak yükledi, o yüzden __dirname tsx altında tanımlıydı, nub altında undefined’dı. Synth yine çıktı ürettiğinden başarısızlık hızlı göründü; oysa handler basitçe yanlıştı. Runner’ı her değiştirdiğinde davranışı yalnızca duvar saatiyle değil, asset sayısı ve template diff’iyle yeniden doğrula.

CDK’nın staging cache’i fonksiyon başına asset’leri çökertebilir. Sekiz fonksiyon tek bir kaynak dizini paylaşıp yalnızca bir tryBundle closure’ının içinde farklılaştığında CDK dizini hashler, tek girdi görür ve tek asset stage eder, böylece sekizi de sessizce aynı kodu deploy eder. Çözüm, her fonksiyonun hash’ini kendi entry dosyasından türeten içerik tabanlı bir AssetHashType.CUSTOM’dır (bu synth sırasında Node üzerinde çalışır, o yüzden node:crypto ve node:fs doğru importlardır):

lambda.Code.fromAsset(handlersDir, {
  assetHashType: AssetHashType.CUSTOM,
  assetHash: crypto
    .createHash('sha256')
    .update(`nubx-esbuild:${entryName}:`)
    .update(fs.readFileSync(entryPath))
    .digest('hex'),
  bundling: {
    image: DockerImage.fromRegistry('dummy-not-used'),
    local: { tryBundle: (outputDir) => runEsbuild(entryPath, outputDir) },
  },
});

Phantom bağımlılıklar patlayana kadar çalışır. node_modules’a hoist edilmiş ama package.json’da olmayan bir pakete import, npx ya da node altında sorunsuz çalışır, sonra hoisting değiştiği gün patlar. nub bunu WARN_PHANTOM_DEP olarak yüzeye çıkardı; düz runner’lar sessiz kaldı. O uyarı, bedavaya gelen bir doğruluk kapısıdır.

Vendor oranları senin projenle temas edince hayatta kalmaz. nub reposu nubx’i npx’ten yaklaşık 19 kat hızlı gösterir; bu CDK dispatch senaryosunda yaklaşık 2,1 kat ölçüldü (çağrı başına nubx artı 78 ms’ye karşı npx artı 167 ms). Başlık tamamen baseline’a ve cache durumuna bağlı. Buradaki mutlak-milisaniye sütunları dahil, tek sayılı her hız iddiasını onu üreten makineye özgü kabul et.

Varsayılan çoğu CDK TypeScript Lambda projesi için geçerlidir: runtime’ı nodejs22.x’te sabitle, NodejsFunction ve mainFields: ['module', 'main'] ile esbuild üzerinden bundle et ve tsx ile synth et. Override’a yalnızca adı konmuş bir koşulda uzan: webpack’ten geçiyorsan ya da cold start yüzlerce fonksiyonda kritikse rspack, yalnızca CI süresi bolken ve artefakt olabildiğince küçük olmalıysa rollup artı terser, ve yalnızca Bun’ı runtime olarak zaten kabul ettiysen Bun’ın toolchain’i. Bu ayrımı reddeden tek kurulum Bun’ın her-katmanda-tek-araç yaklaşımıdır ve takas açıktır: build hızı runtime’ıyla paket halinde gelir. Geri kalan her şey katmanlı kalır ve her katman, prod’a olan uzaklığının gerektirdiği kadar sıkıcı kalır. En ucuz ilk adım, bundler’a hiç dokunmadan önce mevcut NodejsFunction’ına mainFields eklemek ve bundle’ı --metafile ile diff’lemektir.

Kaynaklar

İlgili yazılar