משפרים את הביצועים על ידי הפעלת תלות ופלטים מודרניים של 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 מדור קודם הוא קוד שמתאפיין בהימנעות ספציפית משימוש בכל תכונות השפה שלמעלה. רוב המפתחים כותבים את קוד המקור שלהם באמצעות תחביר מודרני, אבל הם מקמפלים את הכל לתחביר מדור קודם כדי להגדיל את התמיכה בדפדפנים. איסוף קוד לפי תחביר מדור קודם מגדיל את התמיכה בדפדפנים, אבל ההשפעה לרוב קטנה יותר ממה שאנחנו חושבים. במקרים רבים, רמת התמיכה עולה מ-95% ל-98%, אבל העלות גבוהה מאוד:
בדרך כלל, קוד JavaScript מדור קודם גדול יותר ב-20% ומהיר יותר מקוד מודרני מקביל. לרוב, הפערים האלה הולכים וגדלים בגלל ליקויים בכלים והגדרות שגויות.
ספריות מותקנות מהוות עד 90% מקוד JavaScript אופנתי בסביבת הייצור. קוד ספרייה כרוך בעלות עודפת גבוהה יותר של JavaScript מדור קודם בגלל כפילויות של polyfill ושל פונקציות עזר, שניתן להימנע מהן על ידי פרסום קוד מודרני.
JavaScript מודרני ב-npm
לאחרונה, ב-Node.js הוגדר שדה "exports"
סטנדרטי להגדרת נקודות כניסה לחבילה:
{
"exports": "./index.js"
}
מודולים שמפנים לשדה "exports"
מניחים גרסה של Node בגרסה 12.8 לפחות, שתומכת ב-ES2019. כלומר, אפשר לכתוב ב-JavaScript מודרני כל מודול שמצוין באמצעות השדה "exports"
. צרכני החבילות צריכים להניח שמודולים עם השדה "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 מודרני, ולכן חשוב לוודא שכלי ה-build מוגדרים לטיפול בהן. סביר להניח שחלק מחבילות ה-npm שאתם תלויים בהן כבר משתמשות בתכונות מודרניות של השפה. יש כמה אפשרויות לשימוש בקוד מודרני מ-npm בלי לשבור את האפליקציה בדפדפנים ישנים יותר, אבל הרעיון הכללי הוא לגרום למערכת ה-build להמיר את יחסי התלות לאותו יעד תחביר כמו קוד המקור.
webpack
החל מ-webpack 5, אפשר להגדיר את התחביר שבו webpack ישתמש כשיוצר קוד לחבילות ולמודולים. הפעולה הזו לא מבצעת תרגום מקוד מדור קודם (transpile) של הקוד או של יחסי התלות, אלא משפיעה רק על קוד הדבקה שנוצר על ידי webpack. כדי לציין את היעד של תמיכת הדפדפנים, מוסיפים לפרויקט הגדרה של browserslist או עושים זאת ישירות בהגדרת webpack:
module.exports = {
target: ['web', 'es2017'],
};
אפשר גם להגדיר את webpack כך שייצור חבילות אופטימליות בלי פונקציות עטיפה מיותרות כשמטרגטים סביבה מודרנית של מודולים של ES. ההגדרה הזו גם מגדירה את webpack לטעון חבילות של קוד מפוצל באמצעות <script type="module">
.
module.exports = {
target: ['web', 'es2017'],
output: {
module: true,
},
experiments: {
outputModule: true,
},
};
יש כמה יישומי פלאגין של webpack שאפשר להשתמש בהם כדי לקמפל ולשלוח JavaScript מודרני תוך תמיכה בדפדפנים מדור קודם, כמו Optimize Plugin ו-BabelEsmPlugin.
הפלאגין של Optimize
Optimize Plugin הוא פלאגין של webpack שממיר קוד חבילה סופי מ-JavaScript מודרני ל-JavaScript מדור קודם, במקום כל קובץ מקור בנפרד. זוהי הגדרה עצמאית שמאפשרת להניח שהכול הוא JavaScript מודרני בתצורת webpack, ללא הסתעפות מיוחדת למספר פלט או תחבירים.
מאחר שהתוסף של Optimize פועל על חבילות במקום על מודולים נפרדים, הוא מעבד את הקוד של האפליקציה ואת יחסי התלות באותה מידה. כך אפשר להשתמש בבטחה ביחסי תלות מודרניים של JavaScript מ-npm, כי הקוד שלהם יהיה מקובץ ויעבור תרגום לסינטקס הנכון. בנוסף, היא יכולה להיות מהירה יותר מפתרונות מסורתיים שכוללים שני שלבי הידור, ועדיין ליצור חבילות נפרדות לדפדפנים מודרניים ולדפדפנים מדור קודם. שתי קבוצות החבילות תוכננו לטעינה באמצעות תבנית module/nomodule.
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
// ...
plugins: [new OptimizePlugin()],
};
Optimize Plugin
יכול להיות מהיר ויעיל יותר מאשר הגדרות מותאמות אישית של webpack, שבדרך כלל אורזות קוד מודרני וקוד מדור קודם בנפרד. הוא גם מטפל בהרצה של Babel בשבילכם, ומצמצם את החבילות באמצעות Terser עם הגדרות אופטימליות נפרדות לפלט המודרני ולפלט הקודם. לבסוף, ה-polyfills הנדרשים לחבילות הקוד הקודמות שנוצרו מופקדים בסקריפט ייעודי, כך שהם אף פעם לא כפולים או נטענים ללא צורך בדפדפנים חדשים יותר.
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, כי הוא מפעיל שני גרסאות build נפרדות של האפליקציה. עיבוד פעמיים יכול להימשך קצת יותר זמן באפליקציות גדולות, אבל הטכניקה הזו מאפשרת לשלב את BabelEsmPlugin
בצורה חלקה בהגדרות קיימות של webpack, והיא אחת מהאפשרויות הנוחות ביותר שזמינות.
הגדרת 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(),
],
};
לחלופין, אפשר להטמיע את הטכניקה באופן ידני בהגדרות של 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 יש תמיכה מובנית ביצירת כמה קבוצות של חבילות כחלק מ-build יחיד, והוא יוצר קוד מודרני כברירת מחדל. כתוצאה מכך, אפשר להגדיר את Rollup כך שייצור חבילות מודרניות וחבילות מדור קודם עם הפלאגינים הרשמיים שכבר משתמשים בהם.
@rollup/plugin-babel
אם משתמשים ב-Rollup, השיטה getBabelOutputPlugin()
(שסופקת על ידי הפלאגין הרשמי של Babel של Rollup) גורמת לטרנספורמציה של הקוד בחבילות שנוצרו, ולא במודולים נפרדים של המקור.
ב-Rollup יש תמיכה מובנית ביצירת כמה קבוצות של חבילות כחלק מ-build יחיד, כל אחת עם הפלאגינים שלה. אפשר להשתמש בכך כדי ליצור חבילות שונות עבור גרסאות מודרניות וגרסאות קודמות, על ידי העברת כל אחת מהן דרך הגדרה שונה של פלאגין הפלט של 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'],
}),
],
},
],
};
כלים נוספים ל-build
אפשר לשנות את ההגדרות של Rollup ו-Webpack במידה רבה, כלומר בדרך כלל צריך לעדכן את ההגדרות של כל פרויקט כדי לאפשר תחביר JavaScript מודרני ביחסי התלות. יש גם כלי build ברמה גבוהה יותר שמעדיפים מוסכמות וערכי ברירת מחדל על פני הגדרות, כמו Parcel, Snowpack, Vite ו-WMR. רוב הכלים האלה יוצאים מנקודת הנחה שיחסי התלות ב-npm עשויים להכיל תחביר מודרני, והם יתרגמו אותם לרמות התחביר המתאימות במהלך ה-build לצורכי ייצור.
בנוסף לתוספים ייעודיים ל-Webpack ול-Rollup, אפשר להוסיף לכל פרויקט חבילות JavaScript מודרניות עם חלופות מדור קודם באמצעות דילוג לאחור. Devolution הוא כלי עצמאי שממיר את הפלט ממערכת build כדי ליצור וריאנטים של JavaScript מדור קודם, ומאפשר חבילה וטרנספורמציות להניח יעד פלט מודרני.