تحسين الأداء من خلال تفعيل الإصدارات الحديثة من JavaScript ومخرجاتها
يمكن لأكثر من %90 من المتصفحات تشغيل JavaScript الحديث، ولكن يبقى انتشار JavaScript القديم مصدرًا كبيرًا لمشاكل الأداء على الويب اليوم.
لغة JavaScript الحديثة
لا يتم تصنيف JavaScript الحديثة على أنّها رمز مكتوب بإصدار محدّد من مواصفات ECMAScript، بل يتم تصنيفها على أنّها رمز مكتوب ببنية نحوية متوافقة مع جميع المتصفّحات الحديثة. تشكّل متصفحات الويب الحديثة، مثل Chrome وEdge وFirefox وSafari، أكثر من 90% من سوق المتصفّحات، وتشكل المتصفّحات المختلفة التي تعتمد على محرّكات العرض الأساسية نفسها نسبتَين إضافيتين تبلغان 5%. وهذا يعني أنّ %95 من زيارات الويب على مستوى العالم تأتي من المتصفّحات التي تتوافق مع ميزات لغة JavaScript الأكثر استخدامًا خلال آخر 10 سنوات، بما في ذلك:
- الفئات (ES2015)
- الدوال السهمية (ES2015)
- مولّدات كهرباء (ES2015)
- تحديد نطاق الحظر (ES2015)
- تحليل البنية (ES2015)
- مَعلمات Rest وSpread (ES2015)
- الاختصارات المتعلقة بالعناصر (ES2015)
- Async/await (ES2017)
بشكل عام، لا يتوافق توافق الميزات في الإصدارات الأحدث من مواصفات اللغة مع المتصفحات الحديثة. على سبيل المثال، لا تتوفّر العديد من ميزات ES2020 وES2021 إلا في %70 من سوق المتصفّحات، ما يمثّل غالبية المتصفّحات، ولكن ليس بالقدر الكافي للاعتماد على هذه الميزات مباشرةً. ويعني ذلك أنّه على الرغم من أنّ JavaScript "الحديثة" هي هدف متغيّر، فإنّ ES2017 لديه أوسع نطاق من التوافق مع المتصفّحات مع تضمين معظم ميزات البنية الحديثة الشائعة الاستخدام. بعبارة أخرى، ES2017 هي الأقرب إلى البنية الحديثة اليوم.
JavaScript قديم
لغة JavaScript القديمة هي رمز برمجي يتجنّب على وجه التحديد استخدام كل ميزات اللغة المذكورة أعلاه. يكتب معظم المطوّرين رمز المصدر باستخدام بنية جمل حديثة، ولكنهم يجمعون كل المحتوى باستخدام بنية جمل قديمة لزيادة توافق المتصفّح. إنّ الترجمة compiling إلى بنية نحوية قديمة تزيد من توافق المتصفّح، ولكنّ التأثير غالبًا ما يكون أصغر مما نتوقّع. في العديد من الحالات، تزيد نسبة التحسين من 95% تقريبًا إلى 98% مع تحمّل تكلفة كبيرة:
يكون عادةً إصدار JavaScript القديم أكبر وأبطأ بنسبة% 20 تقريبًا من الرموز البرمجية الحديثة المماثلة. غالبًا ما يؤدي نقص الأدوات وخطأ الإعداد إلى توسيع هذه الفجوة بشكل أكبر.
تُمثّل المكتبات المثبَّتة ما يصل إلى %90 من رمز JavaScript المعتاد المخصّص للنشر. يتحمّل رمز المكتبة عبء استخدام JavaScript قديم بسبب حدوث تكرارات polyfill ومساعِدة يمكن تجنبها من خلال نشر رموز برمجية حديثة.
لغة JavaScript الحديثة على npm
مؤخرًا، وحّدت Node.js الحقل "exports"
لتحديد نقاط الإدخال للحزمة:
{
"exports": "./index.js"
}
تشير الوحدات التي يشير إليها الحقل "exports"
إلى إصدار Node لا يقل عن
12.8، والذي يتوافق مع ES2019. وهذا يعني أنّه يمكن كتابة أي وحدة تتم الإشارة إليها باستخدام الحقل
"exports"
بلغة JavaScript الحديثة. على مستخدِمي الحِزم افتراض أنّ الوحدات التي تحتوي على حقل "exports"
تتضمّن رمزًا حديثًا وتحويلها إلى رمز قديم إذا لزم الأمر.
تصميم عصري فقط
إذا كنت تريد نشر حزمة تحتوي على رمز حديث وترك الأمر على العميل
لتحويله إلى رمز قديم عند استخدامه كعنصر تابع، استخدِم الحقل
"exports"
فقط.
{
"name": "foo",
"exports": "./modern.js"
}
تصميم حديث مع تنسيق احتياطي قديم
استخدِم الحقل "exports"
مع "main"
لنشر حِزمك
باستخدام رمز حديث، ولكن أدرِج أيضًا بديلاً لـ ES5 + CommonJS للمتصفّحات
القديمة.
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs"
}
الإصدار الحديث مع تحسينات حِزم ESM وعمليات النسخ الاحتياطي القديمة
بالإضافة إلى تحديد نقطة إدخال CommonJS احتياطية، يمكن استخدام الحقل "module"
للإشارة إلى حزمة احتياطية قديمة مشابهة، ولكن واحدة تستخدم
بنية وحدة JavaScript (import
وexport
).
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs",
"module": "./module.js"
}
تعتمد العديد من أدوات تجميع الحِزم، مثل webpack وRollup، على هذا الحقل للاستفادة
من ميزات الوحدات وتفعيل
إزالة العناصر غير المستخدمة.
لا تزال هذه الحِزمة قديمة ولا تحتوي على أي رمز حديث باستثناء نحو
import
/export
، لذا استخدِم هذا النهج لشحن رمز حديث مع
عنصر احتياطي قديم لا يزال محسّنًا للحِزم.
استخدام JavaScript حديث في التطبيقات
تشكّل التبعيات التابعة لجهات خارجية الغالبية العظمى من رمز برمجة JavaScript النموذجي في تطبيقات الويب. على الرغم من أنّه تم في السابق نشر متطلّبات npm كبنية ES5 لنظام التشغيل القديم، لم يعُد هذا الافتراض آمنًا ويمثّل خطرًا على تحديثات المتطلّبات التي قد تؤدي إلى إيقاف توافق المتصفّح في تطبيقك.
مع انتقال عدد متزايد من حِزم npm إلى JavaScript الحديثة، من المُهم التأكّد من إعداد أدوات الإنشاء للتعامل معها. هناك فرصة جيدة لأن بعض حزم npm التي تعتمد عليها تستخدم بالفعل ميزات اللغة الحديثة. هناك عدد من الخيارات المتاحة لاستخدام الرمز الحديث من npm بدون إيقاف تطبيقك في المتصفّحات القديمة، ولكن الفكرة العامة هي أن يُجري نظام الإنشاء عملية تحويل ترميز للتبعيات إلى الهدف النحوي نفسه المستخدَم في الرمز المصدر.
webpack
اعتبارًا من الإصدار 5 من webpack، أصبح من الممكن الآن ضبط البنية التي سيستخدمها webpack عند إنشاء رمز للحزمات والوحدات. لا يؤدي هذا إلى تحويل التعليمات البرمجية أو التبعيات، إنه يؤثر فقط على التعليمة البرمجية "اللاصقة" التي تم إنشاؤها بواسطة webpack. لتحديد استهداف توافق المتصفّح، أضِف إعدادات browserslist إلى مشروعك، أو نفِّذ ذلك مباشرةً في إعدادات webpack:
module.exports = {
target: ['web', 'es2017'],
};
من الممكن أيضًا ضبط webpack لإنشاء حِزم محسّنة تُهمل الدوالّ المُغلفة غير الضرورية عند استهداف بيئة ES Modules
حديثة. يؤدي ذلك أيضًا إلى ضبط webpack لتحميل حِزم مجزّأة من الرمز البرمجي باستخدام
<script type="module">
.
module.exports = {
target: ['web', 'es2017'],
output: {
module: true,
},
experiments: {
outputModule: true,
},
};
تتوفّر عدة مكونات إضافية لواجهة webpack تتيح تجميع JavaScript الحديثة ونشرها مع مواصلة دعم المتصفّحات القديمة، مثل Optimize Plugin وBabelEsmPlugin.
المكوّن الإضافي "أدوات تحسين الأداء"
مكوّن Optimize الإضافي هو مكوّن إضافي لبرنامج webpack يحوّل الرمز البرمجي المجمّع النهائي من JavaScript الحديث إلى JavaScript القديم بدلاً من كل ملف مصدر فردي. وهو عبارة عن إعداد متكامل يتيح لإعدادات webpack افتراض أنّ كل شيء هو JavaScript حديث بدون تشعّب خاص لملفات الإخراج أو البنى النحوية المتعددة.
بما أنّ "مكوّن Optimize الإضافي" يعمل على الحِزم بدلاً من الوحدات الفردية، فإنه يعالج رمز تطبيقك وعناصر الاعتماد على قدم المساواة. ويجعل ذلك استخدام تبعيات JavaScript الحديثة من npm آمنًا، لأنّ رمزها البرمجي سيتم تجميعه وتحويله إلى بنية الجملة الصحيحة. ويمكن أن يكون أيضًا أسرع من الحلول التقليدية التي تتضمّن خطوتَي تجميع، مع مواصلة إنشاء حِزم منفصلة للمتصفّحات الحديثة والقديمة. تم تصميم مجموعتَي الحِزمتَين لتحميلهما باستخدام نمط الوحدة/الوحدة.
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
// ...
plugins: [new OptimizePlugin()],
};
يمكن أن يكون Optimize Plugin
أسرع وأكثر فعالية من إعدادات webpack
المخصّصة التي تُجمِّع عادةً الرموز البرمجية الحديثة والقديمة بشكل منفصل. ويعمل أيضًا على تنفيذ Babel نيابةً عنك، وتصغير حزم باستخدام Terser مع إعدادات مثالية منفصلة لمخرجات الإصدارات الحديثة والإصدارات القديمة. وأخيرًا، يتم استخراج رموز polyfill التي تحتاجها الحِزم القديمة التي تم إنشاؤها في نص برمجي مخصّص كي لا يتم تكرارها أو تحميلها بدون داعٍ في المتصفّحات الأحدث.
BabelEsmPlugin
BabelEsmPlugin هو مكوّن إضافي لـ webpack يعمل مع @babel/preset-env لإنشاء إصدارات حديثة من الحِزم الحالية لإرسال رمز مُحوَّل أقل إلى المتصفّحات الحديثة. وهو الحلّ الجاهز الأكثر شيوعًا لمعالجة علامة /module/nomodule، ويستخدمه كلّ من Next.js وPreact CLI.
// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');
module.exports = {
//...
module: {
rules: [
// your existing babel-loader configuration:
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
plugins: [new BabelEsmPlugin()],
};
يتيح BabelEsmPlugin
مجموعة كبيرة من إعدادات webpack، لأنّه
يشغّل نسختَين منفصلتَين إلى حدٍ كبير من تطبيقك. يمكن أن يستغرق التحويل البرمجي للمرتَين وقتًا إضافيًا في التطبيقات الكبيرة الحجم، ولكنّ هذه التقنية تتيح دمج "BabelEsmPlugin
" بسلاسة في إعدادات حِزم الويب الحالية، وتجعله أحد الخيارات المتاحة الأكثر ملاءمةً.
ضبط babel-loader لتحويل node_modules
إذا كنت تستخدم babel-loader
بدون أحد المكوّنين الإضافيَين السابقَين،
عليك اتّباع خطوة مهمة لاستخدام وحدات npm
الحديثة لـ JavaScript. يتيح تحديد إعدادَين منفصلَين لـ babel-loader
إمكانية التجميع التلقائي لميزات اللغة الحديثة المتوفّرة في node_modules
إلى
ES2017، مع مواصلة ترجمة رمز الطرف الأول الخاص بك باستخدام المكوّنات الإضافية في Babel
والإعدادات المسبقة المحدّدة في إعدادات مشروعك. لا يؤدي ذلك إلى توليد حزم حديثة وقديمة لإعداد module/nomodule، ولكنه يتيح تثبيت حِزم npm التي تحتوي على JavaScript حديث واستخدامها بدون إيقاف المتصفّحات القديمة.
يستخدم webpack-plugin-modern-npm
هذه التقنية لتجميع ملفات npm التي تحتوي على حقل "exports"
في package.json
، لأنّها قد تحتوي على بنية حديثة:
// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');
module.exports = {
plugins: [
// auto-transpile modern stuff found in node_modules
new ModernNpmPlugin(),
],
};
ويمكنك بدلاً من ذلك تطبيق هذه الطريقة يدويًا في إعدادات حزمة الويب
عن طريق البحث عن حقل "exports"
ضمن package.json
من
الوحدات أثناء حلّها. مع حذف التخزين المؤقت لأسباب تتعلق بالإيجاز، قد يبدو تنفيذ
مخصّص على النحو التالي:
// webpack.config.js
module.exports = {
module: {
rules: [
// Transpile for your own first-party code:
{
test: /\.js$/i,
loader: 'babel-loader',
exclude: /node_modules/,
},
// Transpile modern dependencies:
{
test: /\.js$/i,
include(file) {
let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
try {
return dir && !!require(dir[0] + 'package.json').exports;
} catch (e) {}
},
use: {
loader: 'babel-loader',
options: {
babelrc: false,
configFile: false,
presets: ['@babel/preset-env'],
},
},
},
],
},
};
عند استخدام هذا النهج، عليك التأكّد من أنّ أدوات تصغير الملفات متوافقة مع البنية الحديثة. يتوفّر لكل من Terser
وuglify-es
خيار لتحديد {ecma: 2017}
من أجل الحفاظ على بنية ES2017 ومحاولة إنشائها في بعض الحالات أثناء الضغط والتنسيق.
تجميع
تتوفّر في أداة Rollup ميزة مدمجة لإنشاء مجموعات متعددة من الحِزم كجزء من عملية معالجة واحدة، كما تنشئ الرمز البرمجي الحديث تلقائيًا. نتيجةً لذلك، يمكن ضبط أداة Rollup لإنشاء حِزم حديثة وقديمة باستخدام المكوّنات الإضافية الرسمية التي يُحتمل أن تكون تستخدمها حاليًا.
@rollup/plugin-babel
في حال استخدام Rollup، تُحوِّل طريقةgetBabelOutputPlugin()
(التي يوفّرها مكوّن Babel الإضافي الرسمي في Rollup)
الرمز البرمجي في الحِزم التي تم إنشاؤها بدلاً من الوحدات المصدر الفردية.
تتوفّر ميزة مدمجة في أداة Rollup لإنشاء مجموعات متعددة من الحِزم كجزء من
إصدار واحد، ولكل حزمة منها مكوّنات إضافية خاصة بها. يمكنك استخدام هذا الإجراء لإنشاء حزم مختلفة للإصدارات الحديثة والقديمة من خلال تمرير كل منها من خلال إعدادات مختلفة لإضافة مخرجات Babel:
// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: [
// modern bundles:
{
format: 'es',
plugins: [
getBabelOutputPlugin({
presets: [
[
'@babel/preset-env',
{
targets: {esmodules: true},
bugfixes: true,
loose: true,
},
],
],
}),
],
},
// legacy (ES5) bundles:
{
format: 'amd',
entryFileNames: '[name].legacy.js',
chunkFileNames: '[name]-[hash].legacy.js',
plugins: [
getBabelOutputPlugin({
presets: ['@babel/preset-env'],
}),
],
},
],
};
أدوات إنشاء إضافية
يمكن ضبط Rollup وWebpack بشكل كبير، ما يعني بشكل عام أنّه على كل مشروع تعديل إعداداته لتفعيل بنية JavaScript الحديثة في التبعيات. هناك أيضًا أدوات تصميم ذات مستوى أعلى تفضل الاجتماعات والإعدادات التلقائية على الضبط، مثل Parcel وSnowpack وVite وWMR. تفترض معظم هذه الأدوات أنّ متطلّبات npm قد تحتوي على بنية حديثة، وسيتم تحويلها إلى مستويات البنية المناسبة عند إنشاء الإصدار العلني.
بالإضافة إلى الإضافات المخصّصة لكلّ من webpack وRollup، يمكن إضافة حزم JavaScript الحديثة التي تتضمّن عناصر احتياطية قديمة إلى أي مشروع باستخدام devolution. Devolution هي أداة مستقلة تحول الإخراج من نظام إنشاء لإنشاء أنواع برمجة JavaScript قديمة، ما يسمح بتجميع عمليات التحويل وتحويلها إلى إخراج حديث.