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:
| Katman | Soru | Yeşil alan varsayılanı | Nub’un yeri |
|---|---|---|---|
| 1. Runtime | Lambda’da kodu ne çalıştırır? | stock nodejs22.x | hiçbir yerde; nub runtime değil |
| 2. Bundling | Her handler’ı ne paketler? | NodejsFunction + mainFields ile esbuild | bundler’ı yok; en fazla dispatch (nubx esbuild) |
| 3. Synth runner | cdk synth’i ne çalıştırır? | tsx | CDK’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:
| Bundler | Build (medyan) | Toplam | Gzip | Değerlendirme |
|---|---|---|---|---|
| bun build 1.3.14 | 253 ms | 3.148 KB | 906 KB | en hızlı build; ~90 MB binary ister |
| esbuild 0.25 (varsayılan) | 702 ms | 3.136 KB | 918 KB | CDK varsayılanı; bayrakla küçülür |
esbuild + mainFields | ~700 ms | 2.357 KB | 709 KB | önerilen; bedavaya -%25 |
| rspack 2.1.3 | 1.150 ms | 2.170 KB | 630 KB | en iyi boyut/hız dengesi |
| tsup 8.5.1 | 1.151 ms | 2.385 KB | 711 KB | esbuild sarmalayıcı; ESM çözümleme default |
| rolldown 1.1.5 | 1.853 ms | 2.993 KB | 859 KB | node-CJS’te bugün olgun değil |
| tsdown 0.22.4 | 2.648 ms | 2.993 KB | 859 KB | rolldown ile byte-identical |
| webpack 5.108 | 11.567 ms | 2.570 KB | 707 KB | rspack varken gereksiz |
| rollup 4.62 + terser | ~92.000 ms | 2.118 KB | 610 KB | en küçük, en yavaş (n=1) |
| parcel 2.16.4 | başarısız | — | — | buraya 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:
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:
| Runner | Ham TS başlangıç | CDK app yükü | cdk synth |
|---|---|---|---|
| tsx | 254 ms | 777 ms | ~1,7 s |
| nub 0.4.8 | 112 ms | 1.128 ms | ~2,2 s |
| ts-node | 864 ms | 6.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ğı | Boyut | Init (medyan) |
|---|---|---|
| esbuild, küçük handler | 60 KB | 2,5 ms |
| rollup, s3-report | 389 KB | 26 ms |
| rspack, s3-report | 397 KB | 27 ms |
esbuild + mainFields, s3-report | 479 KB | 39 ms |
| tsup, s3-report | 485 KB | 39 ms |
| rolldown, s3-report | 577 KB | 48 ms |
| esbuild varsayılan, s3-report | 622 KB | 54 ms |
| bun, s3-report | 639 KB | 51 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şlatma | JS dosyası | TS dosyası |
|---|---|---|
| node | 26 ms | — |
| bun | 60 ms | 62 ms |
| nub | 130 ms | 112 ms |
| tsx | 227 ms | 254 ms |
| ts-node | — | 864 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
- nubjs/nub (GitHub) - Hızlı all-in-one Node.js toolkit’i; nubx’in npx’e karşı hız iddiasının ve “değiştirme, güçlendir” duruşunun kaynağı.
- Nub resmi sitesi - Konumlanma ve dokümanlar; nub stock Node’u değiştirmek yerine güçlendirir.
- esbuild: Main fields -
main’in CommonJS,module’ün ESM olması ve ESM’in neden daha iyi tree-shaking sağladığı. - CDK NodejsFunction BundlingOptions.mainFields - CDK prop’u; dokümanlar ES module sürümlerini tercih etmek için
['module', 'main']öneriyor. Varsayılan boş. - Modular packages in AWS SDK for JavaScript - SDK’nın neden hem dist-cjs hem dist-es gönderdiği ve CJS çözümlemenin tree-shaking’i neden bozduğu.
- Optimizing Node.js dependencies in AWS Lambda - Lambda artefaktını ve cold start’ı küçültmek için bundling ve tree-shaking; esbuild öneriyor.
- Rspack: Migrate from webpack - Aynı plugin adları ve config parametreleriyle drop-in webpack uyumluluğu; config-uyumlu iddiasını temellendirir.
- Rolldown - VoidZero’nun Rust bundler’ı; olgunluk ve CJS-hedef durumu için proje evi.
- Understanding the Lambda execution environment lifecycle - Init fazı kodu indirir ve initialization’ı handler dışında çalıştırır; cold-start ve paket-boyut bağlantısını temellendirir.
- aws-cdk aws-lambda-nodejs bundling performance (issue #10286) - CDK’nın esbuild’e geçişinden önceki Parcel bundling performans raporu.
- Nub vs Vite+ (kardeş yazı) - Bu Rust araçlarını katmana göre sıralayan toolchain haritası; bu benchmark onun CDK’ya özgü ölçülmüş adımı.
İlgili yazılar
AppSync subscription'ları yalnızca mutation ile tetiklenir. Downstream BFF olaylarını NONE veri kaynaklı bir mutation'a EventBridge ve CDK ile köprülemeyi inceliyorum.
Mimari ağırlığını runtime'ın init-amortismanına göre seç: single-purpose Lambda'da yalın handler, Lambdalith'te orta, tam OOP/DI yalnızca uzun ömürlü runtime'da.
AWS Lambda fonksiyonlarını nasıl bölmeli: varsayılan single-purpose, single-domain Lambdalith'i hak edilmiş istisna gör, kararı veren platform güçlerini tanı.
DI container'lar, monolitik SDK'lar, god-handler'lar, modül üstü secret çağrıları ve ağır ORM'ler - soğuk başlatmada bedeli ve yerine geçen fonksiyonel yapı.
Bun ve Deno'yu AWS Lambda'da custom runtime ile çalıştırma: gerçek performans benchmark'ları, maliyet analizi ve production deployment pattern'leri.