משפרים את הביצועים על ידי הפעלת תלות ופלטים מודרניים של 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"
מכילים קוד מודרני, ולבצע תרגום מקוד מדור קודם אם יש צורך.
מודרני בלבד
אם אתם רוצים לפרסם חבילה עם קוד מודרני ולהשאיר לצרכנים את האחריות על ההמרה (transpiling) שלה כשהם משתמשים בה כחבילת תלות, השתמשו רק בשדה "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"
}
חבילות Bundle רבות, כמו 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 עם הגדרות אופטימליות נפרדות לפלט המודרני ולפלט הקודם. לבסוף, את הנתונים הפוליטיים הנדרשים על ידי החבילות הקודמות שנוצרו מחולצים לסקריפט ייעודי, כך שהם אף פעם לא משוכפלים או נטענים שלא לצורך בדפדפנים חדשים.
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
להשתלב בצורה חלקה בתצורות קיימות של חבילות אינטרנט, ולכן היא אחת מהאפשרויות הנוחות ביותר שקיימות.
הגדרת babel-loader כדי להמיר את node_modules
אם אתם משתמשים ב-babel-loader
בלי אחד משני יישומי הפלאגין הקודמים, נדרש שלב חשוב כדי לצרוך מודולים מודרניים של JavaScript npm. הגדרת שתי הגדרות נפרדות של 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
אם אתם משתמשים בנכס-על, השיטה 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
יש הרבה אפשרויות הגדרה של אוסף-על וחבילות Webpack. בדרך כלל כל פרויקט צריך לעדכן את ההגדרות שלו כדי להפעיל תחביר JavaScript מודרני ביחסי תלות. יש גם כלי build ברמה גבוהה יותר שמעדיפים מוסכמות וערכי ברירת מחדל על פני הגדרות, כמו Parcel, Snowpack, Vite ו-WMR. רוב הכלים האלה יוצאים מנקודת הנחה שיחסי התלות ב-npm עשויים להכיל תחביר מודרני, והם יתרגמו אותם לרמות התחביר המתאימות במהלך ה-build לצורכי ייצור.
בנוסף לתוספים ייעודיים ל-Webpack ול-Rollup, אפשר להוסיף לכל פרויקט חבילות JavaScript מודרניות עם חלופות מדור קודם באמצעות דילוג לאחור. Devolution הוא כלי עצמאי שממיר את הפלט ממערכת build כדי ליצור וריאנטים של JavaScript מדור קודם, ומאפשר חבילה וטרנספורמציות להניח יעד פלט מודרני.