التخزين المؤقت للبايت كود هو تحسين في وقت البناء يحسن بشكل كبير من وقت بدء التطبيق من خلال الترجمة المسبقة لجافا سكريبت إلى بايت كود. على سبيل المثال، عند تجميع TypeScript's tsc مع تمكين البايت كود، يتحسن وقت البدء بمقدار 2x.
الاستخدام
الاستخدام الأساسي
قم بتمكين التخزين المؤقت للبايت كود باستخدام العلم --bytecode:
bun build ./index.ts --target=bun --bytecode --outdir=./distهذا يولد ملفين:
dist/index.js- جافا سكريبت المجمعةdist/index.jsc- ملف التخزين المؤقت للبايت كود
في وقت التشغيل، يكتشف Bun تلقائيًا ويستخدم ملف .jsc:
bun ./dist/index.js # يستخدم تلقائيًا index.jscمع الملفات التنفيذية المستقلة
عند إنشاء ملفات تنفيذية باستخدام --compile، يتم تضمين البايت كود في الملف الثنائي:
bun build ./cli.ts --compile --bytecode --outfile=mycliالملف التنفيذي الناتج يحتوي على كل من الكود والبايت كود، مما يمنحك أقصى أداء في ملف واحد.
الجمع مع التحسينات الأخرى
يعمل البايت كود بشكل رائع مع التصغير وخرائط المصدر:
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli--minifyيقلل من حجم الكود قبل توليد البايت كود (كود أقل -> بايت كود أقل)--sourcemapيحافظ على الإبلاغ عن الأخطاء (لا تزال الأخطاء تشير إلى المصدر الأصلي)--bytecodeيزيل نفقات التحليل
تأثير الأداء
يتناسب تحسين الأداء مع حجم قاعدة الكود الخاصة بك:
| حجم التطبيق | التحسين النموذجي للبدء |
|---|---|
| CLI صغير (< 100 KB) | أسرع بمقدار 1.5-2x |
| تطبيق متوسط-كبير (> 5 MB) | أسرع بمقدار 2.5x-4x |
التطبيقات الأكبر تستفيد أكثر لأن لديها كودًا أكثر للتحليل.
متى تستخدم البايت كود
رائع لـ:
أدوات CLI
- يتم استدعاؤها بشكل متكرر (أدوات التدقيق، المنسقات، خطافات git)
- وقت البدء هو تجربة المستخدم بأكملها
- يلاحظ المستخدمون الفرق بين بدء 90ms و 45ms
- مثال: مجمع TypeScript، Prettier، ESLint
أدوات البناء ومشغلي المهام
- تعمل مئات أو آلاف المرات أثناء التطوير
- المللي ثانية المحفوظة لكل تشغيل تتراكم بسرعة
- تحسين تجربة المطور
- مثال: نصوص البناء، مشغلي الاختبار، مولدات الكود
الملفات التنفيذية المستقلة
- يتم توزيعها على المستخدمين الذين يهتمون بالأداء السريع
- التوزيع بملف واحد مناسب
- حجم الملف أقل أهمية من وقت البدء
- مثال: CLIs الموزعة عبر npm أو كملفات ثنائية
تخطى ذلك لـ:
- ❌ النصوص الصغيرة
- ❌ الكود الذي يعمل مرة واحدة
- ❌ إصدارات التطوير
- ❌ البيئات المقيدة بالحجم
- ❌ الكود مع top-level await (غير مدعوم)
القيود
CommonJS فقط
يعمل التخزين المؤقت للبايت كود حاليًا مع تنسيق إخراج CommonJS. يحول مجمع Bun تلقائيًا معظم كود ESM إلى CommonJS، لكن top-level await هو الاستثناء:
// هذا يمنع التخزين المؤقت للبايت كود
const data = await fetch("https://api.example.com");
export default data;لماذا: يتطلب top-level await تقييم وحدة غير متزامن، والذي لا يمكن تمثيله في CommonJS. يصبح رسم الوحدة غير متزامن، وينهار نموذج دالة الغلاف CommonJS.
حل بديل: انقل التهيئة غير المتزامنة إلى دالة:
async function init() {
const data = await fetch("https://api.example.com");
return data;
}
export default init;الآن تصدر الوحدة دالة يمكن للمستهلك انتظارها عند الحاجة.
توافق الإصدار
البايت كود غير قابل للنقل عبر إصدارات Bun. يرتبط تنسيق البايت كود بالتمثيل الداخلي لـ JavaScriptCore، والذي يتغير بين الإصدارات.
عند تحديث Bun، يجب إعادة توليد البايت كود:
# بعد تحديث Bun
bun build --bytecode ./index.ts --outdir=./distإذا لم يتطابق البايت كود مع إصدار Bun الحالي، يتم تجاهله تلقائيًا ويعود كودك إلى تحليل مصدر جافا سكريبت. لا يزال تطبيقك يعمل - تفقد فقط تحسين الأداء.
أفضل ممارسة: قم بتوليد البايت كود كجزء من عملية بناء CI/CD. لا تلزم ملفات .jsc في git. أعد توليدها كلما قمت بتحديث Bun.
لا يزال كود المصدر مطلوبًا
- ملف
.js(كود المصدر المجمّع) - ملف
.jsc(التخزين المؤقت للبايت كود)
في وقت التشغيل:
- يقوم Bun بتحميل ملف
.js، ويرى pragma@bytecode، ويتحقق من ملف.jsc - يقوم Bun بتحميل ملف
.jsc - يتحقق Bun من تطابق تجزئة البايت كود مع المصدر
- إذا كان صالحًا، يستخدم Bun البايت كود
- إذا كان غير صالح، يعود Bun إلى تحليل المصدر
البايت كود ليس إخفاءً
البايت كود لا يحجب كود المصدر الخاص بك. إنه تحسين، وليس إجراء أمني.
نشر الإنتاج
Docker
تضمين توليد البايت كود في Dockerfile الخاص بك:
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --bytecode --minify --sourcemap \
--target=bun \
--outdir=./dist \
--compile \
./src/server.ts --outfile=./dist/server
FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /dist/server /app/server
CMD ["./server"]البايت كود مستقل عن البنية.
CI/CD
قم بتوليد البايت كود أثناء خط البناء الخاص بك:
# GitHub Actions
- name: البناء مع البايت كود
run: |
bun install
bun build --bytecode --minify \
--outdir=./dist \
--target=bun \
./src/index.tsالتصحيح
التحقق من استخدام البايت كود
تحقق من وجود ملف .jsc:
ls -lh dist/-rw-r--r-- 1 user staff 245K index.js
-rw-r--r-- 1 user staff 1.1M index.jscيجب أن يكون ملف .jsc أكبر بمقدار 2-8x من ملف .js.
لتسجيل ما إذا كان البايت كود قيد الاستخدام، عيّن BUN_JSC_verboseDiskCache=1 في بيئتك.
عند النجاح، سيسجل شيئًا مثل:
[Disk cache] cache hit for sourceCodeإذا رأيت فقدان ذاكرة التخزين المؤقت، فسيسجل شيئًا مثل:
[Disk cache] cache miss for sourceCodeمن الطبيعي أن يسجل فقدان ذاكرة التخزين المؤقت عدة مرات لأن Bun لا يقوم حاليًا بتخزين كود جافا سكريبت المستخدم في الوحدات المدمجة مؤقتًا.
المشاكل الشائعة
تجاهل البايت كود بصمت: عادة ما يكون ناتجًا عن تحديث إصدار Bun. لا يتطابق إصدار ذاكرة التخزين المؤقت، لذا يتم رفض البايت كود. أعد التوليد للإصلاح.
حجم الملف كبير جدًا: هذا متوقع. فكر في:
- استخدام
--minifyلتقليل حجم الكود قبل توليد البايت كود - ضغط ملفات
.jscلنقل الشبكة (gzip/brotli) - تقييم ما إذا كان مكسب أداء البدء يستحق زيادة الحجم
Top-level await: غير مدعوم. أعد الهيكلة لاستخدام دوال التهيئة غير المتزامنة.
ما هو البايت كود؟
عند تشغيل جافا سكريبت، لا ينفذ محرك جافا سكريبت كود المصدر الخاص بك مباشرة. بدلاً من ذلك، يمر بعدة خطوات:
- التحليل: يقرأ المحرك كود مصدر جافا سكريبت ويحوله إلى شجرة بناء جملة مجردة (AST)
- تجميع البايت كود: يتم تجميع AST في البايت كود - تمثيل منخفض المستوى أسرع في التنفيذ
- التنفيذ: يتم تنفيذ البايت كود بواسطة مترجم المحرك أو مجمع JIT
البايت كود هو تمثيل وسيط - إنه أدنى مستوى من كود مصدر جافا سكريبت، لكنه أعلى مستوى من كود الآلة. فكر فيه كلغة التجميع لجهاز افتراضي. يمثل كل تعليمات بايت كود عملية واحدة مثل "تحميل هذا المتغير" أو "إضافة رقمين" أو "استدعاء هذه الدالة".
يحدث هذا في كل مرة تقوم فيها بتشغيل كودك. إذا كان لديك أداة CLI تعمل 100 مرة في اليوم، يتم تحليل كودك 100 مرة. إذا كان لديك دالة serverless مع بدايات باردة متكررة، يحدث التحليل في كل بداية باردة.
مع التخزين المؤقت للبايت كود، ينقل Bun الخطوتين 1 و 2 إلى خطوة البناء. في وقت التشغيل، يقوم المحرك بتحميل البايت كود المترجم مسبقًا ويقفز مباشرة إلى التنفيذ.
لماذا يجعل التحليل الكسول هذا أفضل
تستخدم محركات جافا سكريبت الحديثة تحسينًا ذكيًا يسمى التحليل الكسول. لا يحللون كل كودك مسبقًا - بدلاً من ذلك، يتم تحليل الدوال فقط عند استدعائها لأول مرة:
// بدون تخزين مؤقت للبايت كود:
function rarely_used() {
// يتم تحليل هذه الدالة المكونة من 500 سطر فقط
// عند استدعائها فعليًا
}
function main() {
console.log("بدء التطبيق");
// لا يتم استدعاء rarely_used() أبدًا، لذا لا يتم تحليلها أبدًا
}هذا يعني أن نفقات التحليل ليست مجرد تكلفة بدء - تحدث طوال عمر تطبيقك عند تنفيذ مسارات كود مختلفة. مع التخزين المؤقت للبايت كود، يتم تجميع جميع الدوال مسبقًا، حتى تلك التي يتم تحليلها بشكل كسول. يحدث عمل التحليل مرة واحدة في وقت البناء بدلاً من توزيعه طوال تنفيذ تطبيقك.
تنسيق البايت كود
داخل ملف .jsc
يحتوي ملف .jsc على بنية بايت كود متسلسلة. فهم ما بداخله يساعد في شرح كل من فوائد الأداء ومقايضة حجم الملف.
قسم الرأس (يتم التحقق منه في كل تحميل):
- إصدار ذاكرة التخزين المؤقت: تجزئة مرتبطة بإصدار إطار JavaScriptCore. يضمن هذا أن البايت كود الذي تم إنشاؤه بإصدار واحد من Bun يعمل فقط مع ذلك الإصدار بالضبط.
- علامة نوع كتلة الكود: تحدد ما إذا كانت هذه كتلة كود Program أو Module أو Eval أو Function.
SourceCodeKey (يتحقق من تطابق البايت كود مع المصدر):
- تجزئة كود المصدر: تجزئة لكود مصدر جافا سكريبت الأصلي. يتحقق Bun من تطابق هذا قبل استخدام البايت كود.
- طول كود المصدر: الطول الدقيق للمصدر، لمزيد من التحقق.
- أعلام التجميع: سياق التجميع الحرج مثل الوضع الصارم، سواء كانت نصًا مقابل وحدة، نوع سياق eval، إلخ. كود المصدر نفسه الذي تم تجميعه بأعلام مختلفة ينتج بايت كود مختلف.
تعليمات البايت كود:
- دفق التعليمات: أكواد عمليات البايت كود الفعلية - التمثيل المترجم لجافا سكريبت الخاص بك. هذا تسلسل متغير الطول لتعليمات البايت كود.
- جدول البيانات الوصفية: كل opcode له بيانات وصفية مرتبطة - أشياء مثل عدادات التنميط، تلميحات النوع، وأعداد التنفيذ (حتى لو لم يتم تعبئتها بعد).
- أهداف القفز: عناوين محسوبة مسبقًا لتدفق التحكم (if/else، الحلقات، عبارات التبديل).
- جداول التبديل: جداول بحث محسنة لعبارات التبديل.
الثوابت والمعرفات:
- مجمع الثوابت: جميع القيم الحرفية في كودك - الأرقام، السلاسل، القيم المنطقية، null، undefined. يتم تخزينها كقيم جافا سكريبت فعلية (JSValues) لذا لا تحتاج إلى تحليلها من المصدر في وقت التشغيل.
- جدول المعرفات: جميع أسماء المتغيرات والدوال المستخدمة في الكود. مخزنة كسلاسل مزالة التكرار.
- علامات تمثيل كود المصدر: أعلامات تشير إلى كيفية تمثيل الثوابت (كأعداد صحيحة، مزدوجة، أعداد صحيحة كبيرة، إلخ).
بيانات الدالة الوصفية (لكل دالة في كودك):
- تخصيص السجلات: عدد السجلات (المتغيرات المحلية) التي تحتاجها الدالة -
thisRegister،scopeRegister،numVars،numCalleeLocals،numParameters. - ميزات الكود: قناع بت لخصائص الدالة: هل هي منشئ؟ دالة سهم؟ هل تستخدم
super؟ هل لديها استدعاءات ذيلية؟ هذه تؤثر على كيفية تنفيذ الدالة. - ميزات النطاق المعجمي: الوضع الصارم والسياق المعجمي الآخر.
- وضع التحليل: الوضع الذي تم فيه تحليل الدالة (عادي، غير متزامن، مولد، مولد غير متزامن).
الهياكل المتداخلة:
- تصريحات وتعابير الدوال: كل دالة متداخلة تحصل على كتلة البايت كود الخاصة بها، بشكل متكرر. ملف به 100 دالة لديه 100 كتلة بايت كود منفصلة، جميعها متداخلة في الهيكل.
- معالجات الاستثناءات: كتل Try/catch/finally مع حدودها وعناوين المعالج المحسوبة مسبقًا.
- معلومات التعبير: يعيد تعيين مواضع البايت كود إلى مواقع كود المصدر للإبلاغ عن الأخطاء والتصحيح.
ما لا يحتويه البايت كود
من المهم، البايت كود لا يضمن كود المصدر الخاص بك. بدلاً من ذلك:
- يتم تخزين مصدر جافا سكريبت بشكل منفصل (في ملف
.js) - يخزن البايت كود فقط تجزئة وطول المصدر
- في وقت التحميل، يتحقق Bun من تطابق البايت كود مع كود المصدر الحالي
لهذا السبب تحتاج إلى نشر كل من ملفي .js و .jsc. ملف .jsc عديم الفائدة بدون ملف .js المقابل له.
المفاضلة: حجم الملف
ملفات البايت كود أكبر بكثير من كود المصدر - عادةً 2-8x أكبر.
لماذا البايت كود أكبر بكثير؟
تعليمات البايت كود مطولة: سطر واحد من جافا سكريبت المصغر قد يترجم إلى عشرات تعليمات البايت كود. على سبيل المثال:
const sum = arr.reduce((a, b) => a + b, 0);يترجم إلى بايت كود الذي:
- يحمل المتغير
arr - يحصل على الخاصية
reduce - ينشئ دالة السهم (التي لديها بايت كود خاص بها)
- يحمل القيمة الأولية
0 - يعد الاتصال بالعدد الصحيح من الوسائط
- ينفذ الاتصال فعليًا
- يخزن النتيجة في
sum
كل من هذه الخطوات هي تعليمات بايت كود منفصلة مع البيانات الوصفية الخاصة بها.
مجمعات الثوابت تخزن كل شيء: كل حرف سلسلة، رقم، اسم خاصية - كل شيء يتم تخزينه في مجمع الثوابت. حتى لو كان كود المصدر الخاص بك يحتوي على "hello" مائة مرة، يخزن مجمع الثوابت مرة واحدة، لكن جدول المعرفات ومراجع الثوابت تضيف نفقات.
بيانات وصفية لكل دالة: كل دالة - حتى الدوال الصغيرة ذات السطر الواحد - تحصل على بيانات وصفية كاملة خاصة بها:
- معلومات تخصيص السجل
- قناع بت ميزات الكود
- وضع التحليل
- معالجات الاستثناءات
- معلومات التعبير للتصحيح
ملف به 1000 دالة صغيرة لديه 1000 مجموعة من البيانات الوصفية.
هياكل بيانات التنميط: على الرغم من عدم تعبئة بيانات التنميط بعد، يتم تخصيص الهياكل لحمل بيانات التنميط. يتضمن هذا:
- فتحات ملف تعريف القيمة (تتبع أنواع التدفق عبر كل عملية)
- فتحات ملف تعريف المصفوفة (تتبع أنماط وصول المصفوفة)
- فتحات ملف تعريف الحساب الثنائي (تتبع أنواع الأرقام في العمليات الرياضية)
- فتحات ملف تعريف الحساب الأحادي
هذه تشغل مساحة حتى عندما تكون فارغة.
تدفق التحكم المحسوب مسبقًا: أهداف القفز، جداول التبديل، وحدود معالج الاستثناءات كلها محسوبة مسبقًا ومخزنة. هذا يجعل التنفيذ أسرع لكن يزيد حجم الملف.
استراتيجيات التخفيف
الضغط: يتم ضغط البايت كود بشكل جيد للغاية مع gzip/brotli (ضغط 60-70%). الهيكل المتكرر والبيانات الوصفية تضغط بكفاءة.
التصغير أولاً: استخدام --minify قبل توليد البايت كود يساعد:
- معرفات أقصر → جدول معرفات أصغر
- إزالة الكود الميت → بايت كود أقل تم إنشاؤه
- طي الثوابت → ثوابت أقل في المجمع
المفاضلة: أنت تتبادل ملفات أكبر بمقدار 2-4x مقابل بدء أسرع بمقدار 2-4x. لـ CLIs، هذا يستحق ذلك عادةً. للخوادم طويلة التشغيل حيث لا تهم بضعة ميغابايت من مساحة القرص، إنها مشكلة أقل.
الإصدار وإمكانية النقل
إمكانية النقل عبر البنية: ✅
البايت كود مستقل عن البنية. يمكنك:
- البناء على macOS ARM64، النشر على Linux x64
- البناء على Linux x64، النشر على AWS Lambda ARM64
- البناء على Windows x64، النشر على macOS ARM64
يحتوي البايت كود على تعليمات مجردة تعمل على أي بنية. تحدث التحسينات الخاصة بالبنية أثناء تجميع JIT في وقت التشغيل، وليس في البايت كود المخزن مؤقتًا.
إمكانية النقل عبر الإصدار: ❌
البايت كود غير مستقر عبر إصدارات Bun. إليك السبب:
تغييرات تنسيق البايت كود: يتطور تنسيق البايت كود لـ JavaScriptCore. تتم إضافة أكواد عمليات جديدة، تتم إزالة أو تغيير القديمة، تتغير هياكل البيانات الوصفية. كل إصدار من JavaScriptCore لديه تنسيق بايت كود مختلف.
التحقق من الإصدار: إصدار ذاكرة التخزين المؤقت في رأس ملف .jsc هو تجزئة لإطار JavaScriptCore. عندما يحمل Bun البايت كود:
- يستخرج إصدار ذاكرة التخزين المؤقت من ملف
.jsc - يحسب إصدار JavaScriptCore الحالي
- إذا لم يتطابقا، يتم رفض البايت كود بصمت
- يعود Bun إلى تحليل مصدر
.js
لا يزال تطبيقك يعمل - تفقد فقط تحسين الأداء.
التدهور السلس: هذا التصميم يعني أن التخزين المؤقت للبايت كود "يفشل بشكل مفتوح" - إذا حدث خطأ ما (عدم تطابق الإصدار، ملف تالف، ملف مفقود)، لا يزال كودك يعمل بشكل طبيعي. قد ترى بدءًا أبطأ، لكنك لن ترى أخطاء.
الباييت كود غير المرتبط مقابل المرتبط
يميز JavaScriptCore بشكل حاسم بين الباييت كود "غير المرتبط" و "المرتبط". هذا الفصل هو ما يجعل التخزين المؤقت للبايت كود ممكنًا:
الباييت كود غير المرتبط (ما يتم تخزينه مؤقتًا)
البايت كود المحفوظ في ملفات .jsc هو بايت كود غير مرتبط. يحتوي على:
- تعليمات الباييت كود المجمعة
- معلومات هيكلية حول الكود
- الثوابت والمعرفات
- معلومات تدفق التحكم
لكنه لا يحتوي على:
- مؤشرات إلى كائنات وقت التشغيل الفعلية
- كود الآلة المجمّع JIT
- بيانات التنميط من التشغيلات السابقة
- معلومات ارتباط الاتصال (أي الدوال تستدعي أي)
البايت كود غير المرتبط ثابت وقابل للمشاركة. يمكن لجميع التنفيذات لنفس الكود الرجوع إلى نفس الباييت كود غير المرتبط.
الباييت كود المرتبط (تنفيذ وقت التشغيل)
عندما يشغل Bun الباييت كود، يقوم "بربطه" - إنشاء غلاف وقت تشغيل يضيف:
- معلومات ارتباط الاتصال: أثناء تشغيل كودك، يتعلم المحرك أي الدوال تستدعي أيًا ويحسن مواقع الاتصال تلك.
- بيانات التنميط: يتتبع المحرك عدد المرات التي يتم فيها تنفيذ كل تعليمات، وما هي أنواع القيم التي تتدفق عبر الكود، وأنماط وصول المصفوفة، إلخ.
- حالة تجميع JIT: إشارات إلى إصدارات JIT الأساسية أو المحسنة (DFG/FTL) المجمعة للكود الساخن.
- كائنات وقت التشغيل: مؤشرات إلى كائنات JavaScript الفعلية، والنماذج الأولية، والنطاقات، إلخ.
يتم إنشاء هذا التمثيل المرتبط جديدًا في كل مرة تقوم فيها بتشغيل كودك. هذا يسمح بـ:
- تخزين العمل المكلف (التحليل والتجميع إلى بايت كود غير مرتبط)
- الاستمرار في جمع بيانات التنميط في وقت التشغيل لتوجيه التحسينات
- الاستمرار في تطبيق تحسينات JIT بناءً على أنماط التنفيذ الفعلية
ينقل التخزين المؤقت للبايت كود العمل المكلف (التحليل وتجميع البايت كود) من وقت التشغيل إلى وقت البناء. بالنسبة للتطبيقات التي تبدأ بشكل متكرر، يمكن أن يقلل هذا من وقت البدء إلى النصف مقابل ملفات أكبر على القرص.
بالنسبة لـ CLIs الإنتاجية وعمليات نشر serverless، يمنحك المزيج من --bytecode --minify --sourcemap أفضل أداء مع الحفاظ على إمكانية التصحيح.