اهتزاز الشجرة: دليل مرجعي
نشرت: 2022-03-10قبل أن نبدأ رحلتنا لمعرفة ماهية اهتزاز الشجرة وكيفية إعداد أنفسنا لتحقيق النجاح معه ، نحتاج إلى فهم الوحدات النمطية الموجودة في نظام JavaScript البيئي.
منذ أيامها الأولى ، ازداد تعقيد برامج JavaScript وعدد المهام التي تؤديها. أصبحت الحاجة إلى تجزئة مثل هذه المهام إلى نطاقات تنفيذ مغلقة واضحة. هذه الأجزاء من المهام ، أو القيم ، هي ما نسميه الوحدات النمطية . الغرض الرئيسي منها هو منع التكرار والاستفادة من إعادة الاستخدام. لذلك ، تم تصميم الهياكل للسماح بمثل هذه الأنواع الخاصة من النطاق ، وكشف قيمها ومهامها ، واستهلاك القيم والمهام الخارجية.
للتعمق في ماهية الوحدات وكيفية عملها ، أوصي بـ "ES Modules: A Cartoon Deep-Dive". ولكن لفهم الفروق الدقيقة في اهتزاز الأشجار واستهلاك الوحدة ، يجب أن يكون التعريف أعلاه كافياً.
ماذا يعني اهتزاز الشجرة في الواقع؟
ببساطة ، يعني اهتزاز الشجرة إزالة الكود الذي لا يمكن الوصول إليه (المعروف أيضًا باسم الكود الميت) من الحزمة. كما تنص وثائق Webpack الإصدار 3:
"يمكنك تخيل التطبيق الخاص بك كشجرة. تمثل شفرة المصدر والمكتبات التي تستخدمها فعليًا الأوراق الحية الخضراء للشجرة. يمثل الرمز الميت الأوراق البنية الميتة للشجرة التي يلتهمها الخريف. من أجل التخلص من الأوراق الميتة ، عليك هز الشجرة ، مما يتسبب في سقوطها ".
تم تعميم المصطلح لأول مرة في مجتمع الواجهة الأمامية بواسطة فريق Rollup. لكن المؤلفين من جميع اللغات الديناميكية يكافحون مع هذه المشكلة منذ وقت مبكر. يمكن إرجاع فكرة خوارزمية اهتزاز الأشجار إلى أوائل التسعينيات على الأقل.
في أرض جافا سكريبت ، أصبح اهتزاز الشجرة ممكنًا منذ مواصفات وحدة ECMAScript (ESM) في ES2015 ، المعروفة سابقًا باسم ES6. منذ ذلك الحين ، تم تمكين اهتزاز الشجرة افتراضيًا في معظم الحزم لأنها تقلل حجم الإخراج دون تغيير سلوك البرنامج.
والسبب الرئيسي لذلك هو أن آليات الإدارة البيئية ثابتة بطبيعتها. دعونا نفصل ما يعنيه ذلك.
وحدات ES مقابل CommonJS
يسبق CommonJS مواصفات ESM ببضع سنوات. لقد حان لمعالجة نقص الدعم للوحدات القابلة لإعادة الاستخدام في نظام JavaScript البيئي. يحتوي CommonJS على وظيفة require()
تجلب وحدة خارجية بناءً على المسار المقدم ، وتضيفها إلى النطاق أثناء وقت التشغيل.
هذا require
function
مثل أي وظيفة أخرى في برنامج يجعل من الصعب بما يكفي لتقييم نتيجة المكالمة في وقت الترجمة. علاوة على ذلك ، فإن إضافة مكالمات require
في أي مكان في الكود أمر ممكن - ملفوفة في استدعاء دالة آخر ، داخل عبارات if / else ، في عبارات التبديل ، إلخ.
مع التعلم والصعوبات التي نتجت عن الاعتماد الواسع لهندسة CommonJS ، استقرت مواصفات ESM على هذه البنية الجديدة ، حيث يتم استيراد الوحدات النمطية وتصديرها عن طريق import
export
الكلمات الرئيسية المعنية. لذلك ، لا مزيد من المكالمات الوظيفية. يُسمح أيضًا بـ ESMs فقط كإعلانات عالية المستوى - لا يمكن دمجها في أي بنية أخرى ، لأنها ثابتة : لا تعتمد ESM على تنفيذ وقت التشغيل.
النطاق والآثار الجانبية
ومع ذلك ، هناك عقبة أخرى يجب أن يتغلب عليها اهتزاز الأشجار لتجنب الانتفاخ: الآثار الجانبية. تعتبر الوظيفة لها آثار جانبية عندما تتغير أو تعتمد على عوامل خارج نطاق التنفيذ. تعتبر الوظيفة ذات الآثار الجانبية غير نقية . ستؤدي الوظيفة البحتة دائمًا إلى نفس النتيجة ، بغض النظر عن السياق أو البيئة التي تم تشغيلها فيها.
const pure = (a:number, b:number) => a + b const impure = (c:number) => window.foo.number + c
يخدم Bundlers غرضهم من خلال تقييم الكود المقدم قدر الإمكان من أجل تحديد ما إذا كانت الوحدة نقية. لكن تقييم الكود أثناء وقت التجميع أو وقت التجميع يمكن أن يستمر فقط حتى الآن. لذلك ، يُفترض أنه لا يمكن التخلص من الحزم ذات الآثار الجانبية بشكل صحيح ، حتى عندما يتعذر الوصول إليها تمامًا.
لهذا السبب ، تقبل الحزم الآن مفتاحًا داخل ملف package.json
للوحدة النمطية يسمح للمطور بإعلان ما إذا كانت الوحدة النمطية ليس لها آثار جانبية. بهذه الطريقة ، يمكن للمطور إلغاء الاشتراك في تقييم الكود وتلميح المجمّع ؛ يمكن حذف الكود الموجود في حزمة معينة إذا لم يكن هناك استيراد يمكن الوصول إليه أو require
بيانًا يرتبط به. لا يؤدي هذا إلى إنشاء حزمة أصغر حجمًا فحسب ، بل يمكنه أيضًا تسريع أوقات التجميع.
{ "name": "my-package", "sideEffects": false }
لذلك ، إذا كنت مطورًا للحزم ، فاستفد بضمير حي من sideEffects
قبل النشر ، وبالطبع قم بمراجعته عند كل إصدار لتجنب أي تغييرات غير متوقعة.
بالإضافة إلى الجذر sideEffects
key ، من الممكن أيضًا تحديد النقاء على أساس كل ملف على حدة ، من خلال التعليق على تعليق مضمن ، /*@__PURE__*/
، لاستدعاء الأسلوب الخاص بك.
const x = */@__PURE__*/eliminated_if_not_called()
أنا أعتبر هذا التعليق التوضيحي المضمّن بمثابة فتحة هروب لمطور المستهلك ، ليتم إجراؤه في حالة عدم إعلان الحزمة عن sideEffects: false
أو في حالة أن المكتبة تقدم بالفعل تأثيرًا جانبيًا على طريقة معينة.
تحسين Webpack
من الإصدار 4 فصاعدًا ، تطلب Webpack تكوينًا أقل تدريجيًا للحصول على أفضل الممارسات. تم دمج وظائف اثنين من المكونات الإضافية في النواة. ولأن فريق التطوير يأخذ حجم الحزمة على محمل الجد ، فقد جعلوا اهتزاز الأشجار أمرًا سهلاً.
إذا لم تكن مصلحًا كثيرًا أو إذا لم يكن للتطبيق الخاص بك حالات خاصة ، فإن هز الشجرة تبعياتك هو مجرد سطر واحد.
يحتوي ملف webpack.config.js
على خاصية جذر تسمى mode
. عندما تكون قيمة هذه الخاصية هي production
، فإنها ستهز الشجرة وستحسن وحداتك بشكل كامل. إلى جانب التخلص من التعليمات البرمجية الميتة باستخدام TerserPlugin
، فإن mode: 'production'
سيمكن الأسماء المشوهة الحتمية للوحدات والأجزاء ، وسينشط المكونات الإضافية التالية:
- استخدام تبعية العلم ،
- وشملت العلم قطع ،
- تسلسل الوحدة ،
- لا تنبعث من الأخطاء.
ليس من قبيل الصدفة أن تكون قيمة التشغيل هي production
. لن ترغب في تحسين تبعياتك بالكامل في بيئة التطوير لأنها ستجعل تصحيح المشكلات أكثر صعوبة. لذلك أود أن أقترح القيام بذلك بإحدى طريقتين.
من ناحية أخرى ، يمكنك تمرير علامة mode
إلى واجهة سطر أوامر Webpack:
# This will override the setting in your webpack.config.js webpack --mode=production
بدلاً من ذلك ، يمكنك استخدام المتغير process.env.NODE_ENV
في webpack.config.js
:
mode: process.env.NODE_ENV === 'production' ? 'production' : development
في هذه الحالة ، يجب أن تتذكر تمرير --NODE_ENV=production
في خط أنابيب النشر.
كلا الأسلوبين عبارة عن تجريد أعلى من التعريف المعروف بلوجين من definePlugin
الإصدار 3 وما بعده. الخيار الذي تختاره لا يحدث فرقًا على الإطلاق.
Webpack الإصدار 3 وما دونه
تجدر الإشارة إلى أن السيناريوهات والأمثلة الواردة في هذا القسم قد لا تنطبق على الإصدارات الحديثة من Webpack والحزم الأخرى. يأخذ هذا القسم في الاعتبار استخدام الإصدار 2 من UglifyJS بدلاً من Terser. UglifyJS هي الحزمة التي تم تقسيم Terser منها ، لذلك قد يختلف تقييم الكود فيما بينها.
نظرًا لأن الإصدار 3 من Webpack وما بعده لا يدعم خاصية sideEffects
في package.json
، يجب تقييم جميع الحزم بالكامل قبل حذف الكود. هذا وحده يجعل النهج أقل فعالية ، ولكن يجب أيضًا مراعاة العديد من المحاذير.
كما ذكرنا سابقًا ، ليس لدى المترجم طريقة لمعرفة ما إذا كانت الحزمة تتلاعب بالنطاق العام. لكن هذا ليس الموقف الوحيد الذي يتخطى فيه اهتزاز الأشجار. هناك سيناريوهات أكثر ضبابية.
خذ مثال الحزمة هذا من وثائق Webpack:
// transform.js import * as mylib from 'mylib'; export const someVar = mylib.transform({ // ... }); export const someOtherVar = mylib.transform({ // ... });
وهنا نقطة دخول حزمة المستهلك:
// index.js import { someVar } from './transforms.js'; // Use `someVar`...
لا توجد طريقة لتحديد ما إذا كان mylib.transform
آثارًا جانبية. لذلك ، لن يتم حذف أي رمز.
فيما يلي مواقف أخرى لها نفس النتيجة:
- استدعاء وظيفة من وحدة طرف ثالث لا يستطيع المترجم فحصها ،
- إعادة تصدير الوظائف المستوردة من وحدات الطرف الثالث.
الأداة التي قد تساعد المترجم في الحصول على عمل اهتزاز الشجرة هي babel-plugin-transform-import. سيقوم بتقسيم جميع الصادرات الأعضاء والمحددة إلى عمليات تصدير افتراضية ، مما يسمح بتقييم الوحدات النمطية بشكل فردي.
// before transformation import { Row, Grid as MyGrid } from 'react-bootstrap'; import { merge } from 'lodash'; // after transformation import Row from 'react-bootstrap/lib/Row'; import MyGrid from 'react-bootstrap/lib/Grid'; import merge from 'lodash/merge';
كما أن لديها خاصية تكوين تحذر المطور لتجنب بيانات الاستيراد المزعجة. إذا كنت تستخدم الإصدار 3 من Webpack أو إصدارًا أعلى ، وقد بذلت العناية الواجبة مع التكوين الأساسي وأضفت المكونات الإضافية الموصى بها ، لكن الحزمة الخاصة بك لا تزال تبدو منتفخة ، فأنا أوصي بتجربة هذه الحزمة.
رفع النطاق وتجميع الأوقات
في وقت CommonJS ، كانت معظم الحزم تقوم ببساطة بلف كل وحدة ضمن إعلان دالة آخر وتعيينها داخل كائن. هذا لا يختلف عن أي كائن خريطة موجود:
(function (modulesMap, entry) { // provided CommonJS runtime })({ "index.js": function (require, module, exports) { let { foo } = require('./foo.js') foo.doStuff() }, "foo.js": function(require, module, exports) { module.exports.foo = { doStuff: () => { console.log('I am foo') } } } }, "index.js")
بصرف النظر عن صعوبة التحليل الثابت ، فإن هذا غير متوافق بشكل أساسي مع ESM ، لأننا رأينا أنه لا يمكننا التفاف بيانات import
export
. لذلك ، في الوقت الحاضر ، يرفع المجمّعون كل وحدة إلى المستوى الأعلى:
// moduleA.js let $moduleA$export$doStuff = () => ({ doStuff: () => {} }) // index.js $moduleA$export$doStuff()
هذا النهج متوافق تمامًا مع ESMs ؛ بالإضافة إلى ذلك ، فهو يسمح بتقييم الكود للتعرف بسهولة على الوحدات التي لم يتم استدعاؤها وإسقاطها. التحذير من هذا النهج هو أنه أثناء التجميع ، يستغرق الأمر وقتًا أطول بكثير لأنه يلامس كل عبارة ويخزن الحزمة في الذاكرة أثناء العملية. هذا سبب كبير يجعل أداء التجميع مصدر قلق أكبر للجميع ولماذا يتم الاستفادة من اللغات المجمعة في أدوات تطوير الويب. على سبيل المثال ، esbuild عبارة عن مجمّع مكتوب بلغة Go ، و SWC عبارة عن مترجم TypeScript مكتوب بلغة Rust يتكامل مع Spark ، وهو مجمع مكتوب أيضًا بلغة Rust.
لفهم رفع النطاق بشكل أفضل ، أوصي بشدة بتوثيق Parcel الإصدار 2.
تجنب النقل المبكر
هناك مشكلة واحدة محددة شائعة إلى حد ما للأسف ويمكن أن تكون مدمرة بالنسبة إلى اهتزاز الأشجار. باختصار ، يحدث ذلك عندما تعمل مع لوادر خاصة ، وتقوم بدمج مترجمين مختلفين في المجمّع الخاص بك. التركيبات الشائعة هي TypeScript و Babel و Webpack - في جميع التباديل الممكنة.
يمتلك كل من Babel و TypeScript مترجمين خاصين بهما ، وتسمح أدوات التحميل الخاصة بهما للمطور باستخدامهما ، لسهولة التكامل. وهنا يكمن التهديد الخفي.
تصل هذه المجمعات إلى التعليمات البرمجية الخاصة بك قبل تحسين الكود. وسواء أكان ذلك افتراضيًا أو خاطئًا ، فإن هؤلاء المترجمين غالبًا ما يخرجون وحدات CommonJS بدلاً من ESMs. كما هو مذكور في القسم السابق ، تعتبر وحدات CommonJS النمطية ديناميكية ، وبالتالي لا يمكن تقييمها بشكل صحيح للتخلص من الشفرة الميتة.
أصبح هذا السيناريو أكثر شيوعًا في الوقت الحاضر ، مع نمو التطبيقات "المتشابهة" (أي التطبيقات التي تشغل نفس الكود من جانب الخادم والعميل). نظرًا لأن Node.js ليس لديه دعم قياسي لـ ESM حتى الآن ، فعندما يتم استهداف المجمعين لبيئة node
، فإنهم يخرجون CommonJS.
لذا ، تأكد من التحقق من الكود الذي تتلقاه خوارزمية التحسين .
قائمة مراجعة اهتزاز الشجرة
الآن بعد أن عرفت خصوصيات وعموميات كيفية عمل التجميع وهز الشجرة ، دعنا نرسم لأنفسنا قائمة مرجعية يمكنك طباعتها في مكان ما في متناول يديك عند إعادة النظر في التنفيذ الحالي وقاعدة التعليمات البرمجية. نأمل أن يوفر لك هذا الوقت ويسمح لك بتحسين ليس فقط الأداء المتصور لشفرتك ، ولكن ربما حتى أوقات بناء خط الأنابيب الخاص بك!
- استخدم ESM ، وليس فقط في قاعدة التعليمات البرمجية الخاصة بك ، ولكن أيضًا تفضل الحزم التي تُخرج ESM كمواد مستهلكة لها.
- تأكد من أنك تعرف بالضبط (إن وجدت) من
sideEffects
لم تعلن الآثار الجانبية أو اجعلها مضبوطة على أنهاtrue
. - استفد من التعليق التوضيحي المضمّن للإعلان عن استدعاءات الطريقة النقية عند استهلاك الحزم ذات الآثار الجانبية.
- إذا كنت تقوم بإخراج وحدات CommonJS ، فتأكد من تحسين الحزمة قبل تحويل عبارات الاستيراد والتصدير.
تأليف الحزمة
نأمل في هذه المرحلة أن نتفق جميعًا على أن ESMs هي الطريق إلى الأمام في نظام JavaScript البيئي. كما هو الحال دائمًا في تطوير البرامج ، يمكن أن تكون الانتقالات صعبة. لحسن الحظ ، يمكن لمؤلفي الحزم اعتماد إجراءات غير منقطعة لتسهيل الترحيل السريع والسلس لمستخدميهم.
مع بعض الإضافات الصغيرة إلى package.json
، ستتمكن الحزمة الخاصة بك من إخبار الحزم بالبيئات التي تدعمها الحزمة وكيفية دعمها بشكل أفضل. فيما يلي قائمة مرجعية من Skypack:
- قم بتضمين تصدير ESM.
- أضف
"type": "module"
. - حدد نقطة دخول من خلال
"module": "./path/entry.js"
(اتفاقية المجتمع).
وإليك مثال يظهر عند اتباع أفضل الممارسات وترغب في دعم بيئتي الويب و Node.js:
{ // ... "main": "./index-cjs.js", "module": "./index-esm.js", "exports": { "require": "./index-cjs.js", "import": "./index-esm.js" } // ... }
بالإضافة إلى ذلك ، قدم فريق Skypack درجة جودة الحزمة كمعيار لتحديد ما إذا كانت حزمة معينة قد تم إعدادها للاستمرارية وأفضل الممارسات. الأداة مفتوحة المصدر على GitHub ويمكن إضافتها باعتبارها devDependency
إلى الحزمة الخاصة بك لإجراء الفحوصات بسهولة قبل كل إصدار.
تغليف
آمل أن تكون هذه المقالة مفيدة لك. إذا كان الأمر كذلك ، ففكر في مشاركته مع شبكتك. إنني أتطلع إلى التفاعل معك في التعليقات أو على Twitter.
موارد مفيدة
مقالات وتوثيق
- "ES Modules: A Cartoon Deep-Dive" ، لين كلارك ، Mozilla Hacks
- "شجرة تهز" ، Webpack
- "التكوين" ، Webpack
- "التحسين" ، Webpack
- "نطاق الرفع" ، وثائق الطرود الإصدار 2
المشاريع والأدوات
- تيرسير
- استيراد بابل البرنامج المساعد تحويل
- Skypack
- حزمة الويب
- قطعة
- تراكم
- esbuild
- SWC
- فحص العبوة