تم إطلاق Next.js 8 مؤخرًا. تضمن هذا الإصدار تحسينًا كبيرًا في استخدام الذاكرة أثناء عملية البناء. ستناقش هذه المقالة كيفية مساهمتنا في تحسين أداء Webpack للمجتمع.
يعتمد Next.js على أدوات مثل Webpack و Babel دون الحاجة إلى تكوين مسبق. هدفه الرئيسي هو مساعدتك على التركيز على ما يهم: كود التطبيق الخاص بك.
تتكون التطبيقات الحديثة من صفحة واحدة أو أكثر. على سبيل المثال: الصفحة الرئيسية، مدونة، لوحة تحكم، أو قائمة منتجات.
مع Next.js، تصبح هذه الصفحات ملفات في دليل خاص باسم pages
في جذر المشروع.
على سبيل المثال: الملف pages/about.js
يتوافق مع الرابط /about
.
أحد القيود الرئيسية في تصميم الإطار هو أنه يجب أن يعمل بكفاءة سواء كان لديك صفحة واحدة أو آلاف الصفحات.
أثناء تنفيذ Next.js بدون خادم (Serverless)، أصبح واضحًا أن تشغيل أمر next build
على مشروع يحتوي على مئات الصفحات يؤدي إلى استهلاك عالي للذاكرة. في بعض الأحيان يتجاوز حد الذاكرة البالغ 1.4 جيجابايت الذي يفرضه Node.js.
بدأنا بتحليل استخدام الذاكرة أثناء عملية البناء باستخدام أدوات مطوري Chrome.
في نتائج التحليل، اكتشفنا نقطة يقوم فيها Webpack بتخصيص 548 ميجابايت من الذاكرة دفعة واحدة.
كانت كمية الذاكرة المخصصة مرتبطة مباشرة بعدد الصفحات، مما يعني أن المزيد من الصفحات يؤدي إلى استخدام أكبر للذاكرة.
أظهرت أداة تحليل الذاكرة في Chrome تخصيص 548 ميجابايت دفعة واحدة
من خلال تتبع مسار التنفيذ في تحليل الذاكرة، تمكنا من تحديد الوظيفة التي تسبب في ارتفاع استخدام الذاكرة.
جاء التخصيص من استدعاء طريقة source.source()
التي تولد الملف النهائي وتخزنه في الذاكرة.
لكن بالنظر إلى الوظيفة التي تستدعي طريقة source()
، يمكنك أن ترى أن compilation.assets
كان يتم تكراره باستخدام asyncLib.forEach
. مما يعني أن الوظيفة المقدمة سيتم استدعاؤها لكل ملف في مصفوفة compilation.assets
في نفس الوقت.
هذا يعني أنه إذا كان هناك 100 صفحة مثلاً، وكل صفحة يجب كتابتها على القرص، فإن الكود أعلاه سيحاول كتابة جميع الملفات الـ 100 في نفس الوقت، بما في ذلك توليد جميع الملفات الـ 100 دفعة واحدة.
الحل لهذه المشكلة هو استخدام إشارة مرور (Semaphore) للحد من عدد الكتابات المتزامنة. عادةً نستخدم async-sema لهذا الغرض، ولكن في هذه الحالة كان Webpack يحتوي بالفعل على طريقة مناسبة في neo-async:
asyncLib.forEach(compilation.assets, (source, file, callback) => {
// etc
});
الكود السابق الذي كان ينفذ الوظيفة بشكل متزامن لجميع الأصول
asyncLib.forEachLimit(compilation.assets, 15, (source, file, callback) => {
// etc
});
الكود الجديد الذي ينفذ الوظيفة بشكل متزامن بحد أقصى 15 في نفس الوقت
بعد تطبيق هذا الحد من التزامن وتحليل استخدام ذاكرة البناء مرة أخرى، رأينا أن تخصيص الذاكرة انقسم إلى أجزاء أصغر بحجم 34 ميجابايت.
أظهر المحلل الآن أجزاء بحجم 34 ميجابايت يتم تخصيصها بمرور الوقت
أظهر هذا التغيير نتائج واعدة، ولكن في الممارسة العملية استمر نفاد الذاكرة أثناء البناء، لذا واصلنا التحليل والتحقيق في المشكلة.
من خلال فحص أكثر تعمقًا لتحليل الذاكرة، لاحظنا أنه بعد استدعاء طريقة source.source()
لم يتم تنظيف الذاكرة (جمع القمامة) بعد ذلك.
في Webpack، تكون الأصول عادةً نسخًا من فئات المصدر (Source classes). جميع هذه الفئات تنفذ طريقة source()
التي تولد مصدر الملف.
أظهر التحليل أن العديد من الأصول كانت نسخًا من CachedSource
. تعمل CachedSource
عن طريق تخزين نتيجة source()
في الذاكرة حتى يتم التخلص من الأصل.
بفحص إضافات Webpack التي يستخدمها Next.js، وجدنا أنه ليس لدينا إضافات تستدعي source()
بعد أن يقوم Webpack بكتابة الملف، مما يعني أن تخزين القيمة المكتوبة لم يكن له فائدة.
بعد التعاون مع توبياس كوبرز، قام بتطبيق خيار جديد يسمى output.futureEmitAssets
الذي يسمح بالاشتراك في سلوك كتابة الأصول الجديد.
مع هذا السلوك الجديد، انخفض حجم الأجزاء المخصصة إلى 182 كيلوبايت بمرور الوقت.
بعد كل التحسينات، يظهر المحلل أجزاء بحجم 184 كيلوبايت يتم تخصيصها بمرور الوقت
يحتوي Next.js 8 بالفعل على كل هذه التحسينات مدمجة فيه. لا حاجة لتغيير أي شيء عند استخدام Next.js.
تم تقديم هذا التحسين في Webpack، مما يعني أن ليس فقط مستخدمي Next.js، ولكن جميع مستخدمي Webpack سيستفيدون من هذه التحسينات.
سنواصل العمل بنشاط لتحسين استخدام الذاكرة وأداء Next.js وWebpack.