تحسين الأداء من خلال تفعيل الإصدارات الحديثة من JavaScript ومخرجاتها
يمكن لأكثر من% 90 من المتصفحات تشغيل JavaScript الحديث، ولكن يبقى انتشار JavaScript القديم مصدرًا كبيرًا لمشاكل الأداء على الويب اليوم.
لغة JavaScript الحديثة
لا يتم تصنيف JavaScript الحديثة على أنّها رمز مكتوب بإصدار محدّد من مواصفات ECMAScript، بل يتم تصنيفها على أنّها رمز مكتوب ببنية نحوية متوافقة مع جميع المتصفّحات الحديثة. تشكّل متصفحات الويب الحديثة، مثل Chrome وEdge وFirefox وSafari، أكثر من 90% من سوق المتصفّحات، وتشكل المتصفّحات المختلفة التي تعتمد على محرّكات العرض الأساسية نفسها نسبتَين إضافيتين تبلغان 5%. وهذا يعني أنّ% 95 من زيارات الويب على مستوى العالم تأتي من المتصفّحات التي تتوافق مع ميزات لغة JavaScript الأكثر استخدامًا خلال آخر 10 سنوات، بما في ذلك:
- الفئات (ES2015)
- دوال Arrow (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.
مكوّن "أدوات تحسين الأداء من Google" الإضافي
مكوّن Optimize الإضافي هو مكوّن إضافي لبرنامج webpack يحوّل الرمز البرمجي المجمّع النهائي من JavaScript الحديث إلى JavaScript القديم بدلاً من كل ملف مصدر فردي. وهو عبارة عن إعداد متكامل يتيح لإعدادات webpack افتراض أنّ كل شيء هو JavaScript حديث بدون تشعّب خاص لملفات الإخراج أو البنى النحوية المتعددة.
بما أنّ "مكوّن Optimize الإضافي" يعمل على الحِزم بدلاً من الوحدات الفردية، فإنه يعالج رمز تطبيقك وعناصر الاعتماد على قدم المساواة. ويجعل ذلك استخدام تبعيات JavaScript الحديثة من npm آمنًا، لأنّ رمزها البرمجي سيتم تجميعه وتحويله إلى بنية الجملة الصحيحة. ويمكن أن يكون أيضًا أسرع من الحلول التقليدية التي تتضمّن خطوتَي تجميع، مع مواصلة إنشاء حِزم منفصلة للمتصفّحات الحديثة والقديمة. تم تصميم مجموعتَي الحِزم لتحميلهما باستخدام نمط module/nomodule.
// 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
بالدمج بسلاسة في إعدادات webpack الحالية
وتجعله أحد الخيارات الأكثر ملاءمةً المتاحة.
ضبط babel-loader لتحويل node_modules
إذا كنت تستخدم babel-loader
بدون أحد المكوّنين الإضافيَين السابقَين،
عليك اتّباع خطوة مهمة لاستخدام وحدات npm
الحديثة لـ JavaScript. يتيح تحديد إعدادَين منفصلَين لـ babel-loader
compile تلقائيًا ميزات اللغة الحديثة المتوفّرة في node_modules
إلى ECMAScript 2017، مع مواصلة تحويل رمز الطرف الأول باستخدام المكوّنات الإضافية ومقاييس 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(),
],
};
بدلاً من ذلك، يمكنك تنفيذ هذه الطريقة يدويًا في إعدادات webpack
من خلال البحث عن حقل "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()
method
(التي يوفّرها مكوّن 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 قديمة، ما يتيح التجميع والتحويلات لإنشاء إخراج حديث.