הקטנת גודל ממשק הקצה

איך להשתמש ב-Webpack כדי ליצור אפליקציה קטנה ככל האפשר

אחד הדברים הראשונים שצריך לעשות כשמבצעים אופטימיזציה לאפליקציה הוא להגדיר אותה לקטן ככל האפשר. כך יש לעשות זאת באמצעות Webpack.

שימוש במצב הייצור (Webpack 4 בלבד)

הדגל החדש של mode הוצג ב-Webpack 4. אפשר להגדיר את הדגל הזה ל-'development' או ל-'production' כדי לרמז על Webpack שאתם יוצרים את האפליקציה בסביבה ספציפית:

// webpack.config.js
module.exports = {
  mode: 'production',
};

חשוב להפעיל את המצב production כשיוצרים את האפליקציה לסביבת הייצור. כך תוכלו לבצע אופטימיזציות כמו הקטנה, הסרה של קוד לפיתוח בלבד בספריות ועוד.

קריאה נוספת

הפעלת ההקטנה

הקטנה מתבצעת כשדוחסים את הקוד על ידי הסרת רווחים מיותרים, קיצור שמות של משתנים וכו'. תוכל לעשות זאת כך:

// Original code
function map(array, iteratee) {
  let index = -1;
  const length = array == null ? 0 : array.length;
  const result = new Array(length);

  while (++index < length) {
    result[index] = iteratee(array[index], index, array);
  }
  return result;
}

// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

ב-Webpack יש תמיכה בשתי דרכים להקטנת הקוד: הקטנה ברמת החבילה ואפשרויות ספציפיות לטעינת נתונים. צריך להשתמש בהן בו-זמנית.

הקטנה ברמת החבילה

ההקטנה ברמת החבילה דוחסת את כל החבילה אחרי ההידור. ככה זה עובד:

  1. כותבים את הקוד כך:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. חבילת Webpack משלבת את הנתונים הבאים בערך:

    // bundle.js (part of)
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony export (immutable) */ __webpack_exports__["render"] = render;
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1);
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
    __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__);
    
    function render(data, target) {
    console.log('Rendered!');
    }
    
  3. כלי מיני דוחס אותו לערך הבא:

    // minified bundle.js (part of)
    "use strict";function t(e,n){console.log("Rendered!")}
    Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
    

ב-Webpack 4,ההקטנה ברמת החבילה מופעלת באופן אוטומטי – גם במצב ייצור וגם בלעדיה. הוא משתמש בכלי למזעור UglifyJS מאחורי הקלעים. (אם תצטרכו להשבית את ההקטנה, תוכלו פשוט להשתמש במצב הפיתוח או להעביר את false לאפשרות optimization.minimize).

ב-webpack 3, צריך להשתמש בפלאגין UglifyJS ישירות. הפלאגין מגיע עם חבילת Webpack. כדי להפעיל אותו, צריך להוסיף אותו לקטע plugins בהגדרה:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
  ],
};

אפשרויות ספציפיות לטוען

הדרך השנייה להקטנת הקוד היא אפשרויות ספציפיות לטוען (מהו טוען). בעזרת אפשרויות של כלי הטעינה תוכלו לדחוס נתונים שהכלי המזעור לא יכול להקטין. לדוגמה, כשמייבאים קובץ CSS עם css-loader, הקובץ עובר הידור למחרוזת:

/* comments.css */
.comment {
  color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n  color: black;\r\n}",""]);

כלי ההגדלה לא יכול לדחוס את הקוד הזה כי הוא מחרוזת. כדי להקטין את תוכן הקובץ, אנחנו צריכים להגדיר את מערכת הטעינה כך:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
};

קריאה נוספת

יש לציין NODE_ENV=production

דרך נוספת להקטין את הגודל של ממשק הקצה היא להגדיר את NODE_ENV משתנה הסביבה בקוד לערך production.

ספריות קוראות את המשתנה NODE_ENV כדי לזהות באיזה מצב הן צריכות לעבוד – בפיתוח או בסביבת הייצור. ספריות מסוימות פועלות באופן שונה על סמך משתנה זה. לדוגמה, כאשר NODE_ENV לא מוגדר לערך production, Vue.js מבצע בדיקות נוספות ומדפיס אזהרות:

// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.');
}
// …

React פועל באופן דומה – הוא טוען גרסת פיתוח שכוללת את האזהרות:

// react/index.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

// react/cjs/react.development.js
// …
warning$3(
    componentClass.getDefaultProps.isReactClassApproved,
    'getDefaultProps is only used on classic React.createClass ' +
    'definitions. Use a static property named `defaultProps` instead.'
);
// …

בדרך כלל בדיקות ואזהרות כאלה לא נחוצות בסביבת הייצור, אבל הן נשארות בקוד ומגדילות את גודל הספרייה. בחבילה 4, מסירים אותם על ידי הוספת האפשרות optimization.nodeEnv: 'production':

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
};

בחבילה 3, משתמשים ב-DefinePlugin במקום:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"'
    }),
    new webpack.optimize.UglifyJsPlugin()
  ]
};

האפשרות optimization.nodeEnv וגם DefinePlugin פועלות באותו אופן – הם מחליפים את כל המופעים של process.env.NODE_ENV בערך שצוין. באמצעות ההגדרות שלמעלה:

  1. ה-Webpack יחליף את כל המופעים של process.env.NODE_ENV ב-"production":

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if (process.env.NODE_ENV !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    
  2. לאחר מכן, המזעור יסיר את כל ההסתעפויות האלה ב-if, כי "production" !== 'production' הוא תמיד FALSE, והפלאגין מבין שהקוד בתוך ההסתעפויות האלה אף פעם לא יפעל:

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    }
    

קריאה נוספת

שימוש במודולים של ES

הדרך הבאה להקטין את הגודל של ממשק הקצה היא להשתמש במודולים ES.

כשמשתמשים במודולים של ES, ל-webpack יש אפשרות לבצע ניעור עצים. ניעור עצים מתרחש כש-bundler חוצה את כל עץ התלות, בודק באילו יחסי תלות נעשה שימוש ומסיר קישורים שלא נמצאים בשימוש. כך, אם משתמשים בתחביר של מודול ES, webpack יכול למחוק את הקוד שלא נמצא בשימוש:

  1. אתם כותבים קובץ עם מספר פעולות ייצוא, אבל באפליקציה נעשה שימוש רק באחד מהם:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. ה-Webpack מבין שלא נעשה שימוש ב-commentRestEndpoint ולא יוצר נקודת ייצוא נפרדת בחבילה:

    // bundle.js (part that corresponds to comments.js)
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const render = () => { return 'Rendered!'; };
    /* harmony export (immutable) */ __webpack_exports__["a"] = render;
    
    const commentRestEndpoint = '/rest/comments';
    /* unused harmony export commentRestEndpoint */
    })
    
  3. המזעור מסיר את המשתנה שלא נמצא בשימוש:

    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
    

התכונה הזו פועלת גם בספריות אם הן נכתבו באמצעות מודולים של ES.

עם זאת, לא צריך להשתמש בדיוק בכלי המזעור המובנה של Webpack (UglifyJsPlugin). כל כלי מזעור שתומך בהסרת קוד מת (למשל הפלאגין Babel Minify או הפלאגין Google Closure Compiler) יעשה את העבודה.

קריאה נוספת

לבצע אופטימיזציה של תמונות

תמונות מהוות יותר מחצי מגודל הדף. הם אמנם לא חשובים כמו JavaScript (למשל, הם לא חוסמים את העיבוד), אבל הם עדיין אוכלים חלק גדול מרוחב הפס. כדאי להשתמש ב-url-loader, ב-svg-url-loader וב-image-webpack-loader כדי לבצע אופטימיזציה בחבילה באינטרנט.

url-loader מכניסים קבצים סטטיים קטנים לאפליקציה. בלי הגדרה, צריך למקם אותו ליד החבילה המהודרת, ולהחזיר כתובת URL של אותו קובץ. עם זאת, אם נציין את האפשרות limit, קבצים שקטנים מהמגבלה הזו יקודדו ככתובת URL של נתוני Base64 ויחזירו את כתובת ה-URL הזו. הפעולה הזו מטמיעה את התמונה בקוד ה-JavaScript ושומרת בקשת HTTP:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

svg-url-loader פועל בדיוק כמו url-loader – אבל הוא מקודד קבצים עם קידוד כתובות URL במקום עם Base64. האפשרות הזו שימושית בתמונות SVG – היות שקובצי SVG הם פשוט טקסט פשוט, הקידוד הזה אפקטיבי יותר מבחינת גודל.

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: "svg-url-loader",
        options: {
          limit: 10 * 1024,
          noquotes: true
        }
      }
    ]
  }
};

image-webpack-loader דוחס את התמונות שעוברות בו. הוא תומך בתמונות JPG, PNG, GIF ו-SVG, ולכן נשתמש בו בכל הסוגים האלה.

כלי הטעינה הזה לא מטמיע תמונות באפליקציה, ולכן הוא צריך לפעול בשילוב עם url-loader ו-svg-url-loader. כדי למנוע הדבקת העתקה בשני הכללים (אחד לתמונות JPG/PNG/GIF ואחד לתמונות בפורמט SVG), נכלול את הטוען הזה בכלל נפרד באמצעות enforce: 'pre':

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        loader: 'image-webpack-loader',
        // This will apply the loader before the other ones
        enforce: 'pre'
      }
    ]
  }
};

הגדרות ברירת המחדל של הטוען כבר מוכנות, אבל אם תרצו לבצע הגדרה נוספת, עיינו באפשרויות הפלאגין. כדי לבחור אילו אפשרויות לפרט, תוכלו לעיין במדריך מצוין לאופטימיזציה של תמונות של Addy Osmani.

קריאה נוספת

אופטימיזציה של יחסי תלות

יותר מחצי מהגודל הממוצע של JavaScript מגיע מיחסי תלות, ויכול להיות שחלק מהגודל הזה הוא מיותר.

לדוגמה, Lodash (נכון לגרסה 4.17.4) מוסיפה לחבילה 72KB של קוד מוקטן. אבל אם משתמשים רק ב-20 מהשיטות שלו, אז כ-65KB של קוד מוקטן לא יעזרו כלום.

דוגמה נוספת היא רגע.js. בגרסה 2.19.1 היא צורכת 223KB של קוד מוקטן, וזה נתון עצום – הגודל הממוצע של JavaScript בדף היה 452KB באוקטובר 2017. עם זאת, 170KB בגודל הזה יהיו קובצי לוקליזציה. אם לא משתמשים ב-amount.js בכמה שפות, הקבצים האלה ינפו את החבילה ללא מטרה.

את כל יחסי התלות האלה אפשר לשפר בקלות. ריכזנו גישות לאופטימיזציה במאגר ב-GitHub – כדאי לראות!

הפעלה של שרשור מודולים של מודולים של ES (נקרא גם העלאת היקף)

כשיוצרים חבילה, הפונקציה Webpack ממירה כל מודול לפונקציה:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// bundle.js (part  of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
  var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
  Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_exports__["a"] = render;
  function render(data, target) {
    console.log('Rendered!');
  }
})

בעבר, התהליך הזה נדרש כדי לבודד בין מודולים של CommonJS/AMD זה לזה. עם זאת, הדבר הוסיף גודל ותקורת ביצועים לכל מודול.

ב-Webpack 2 יש תמיכה במודולים של ES. בניגוד למודולים של CommonJS ו-AMD, אפשר לצרף אותם בחבילה בלי לארוז כל אחד מהם באמצעות פונקציה. קיבוץ Webpack 3 מאפשר קיבוץ כזה – עם שרשור מודולים. כך פועל שרשור המודול:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

  // CONCATENATED MODULE: ./comments.js
    function render(data, target) {
    console.log('Rendered!');
  }

  // CONCATENATED MODULE: ./index.js
  render();
})

רואה את ההבדל? בחבילה הרגילה, מודול 0 דרש render ממודול 1. באמצעות שרשור המודול, require פשוט מוחלף בפונקציה הנדרשת ומודול 1 מוסר. החבילה כוללת פחות מודולים, ופחות תקורה של מודולים.

כדי להפעיל את ההתנהגות הזו, בחבילה 4, מפעילים את האפשרות optimization.concatenateModules:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    concatenateModules: true
  }
};

בחבילה 3, משתמשים ב-ModuleConcatenationPlugin:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

קריאה נוספת

אם יש לכם גם קוד חבילת Webpack וגם קוד אחר, צריך להשתמש ב-externals

אם יש לכם פרויקט גדול, חלק מהקוד הונפק באמצעות webpack וחלק מהקוד לא. כמו באתר אירוח סרטונים, שבו הווידג'ט של הנגן עשוי להיבנה באמצעות חבילת אינטרנט, והדף שמסביבו עשוי להיות לא:

צילום מסך של אתר לאירוח סרטונים
(אתר לאירוח סרטונים אקראי לחלוטין)

אם לשני קטעי הקוד יש יחסי תלות משותפים, אפשר לשתף אותם כדי למנוע הורדה של הקוד כמה פעמים. עושים זאת באמצעות האפשרות externals של ה-Webpack – הוא מחליף את המודולים במשתנים או בייבוא חיצוני אחר.

אם יחסי התלות זמינים ב-window

אם הקוד שאינו של Webpack מסתמך על יחסי תלות שזמינים כמשתנים ב-window, צריך לתת כינוי לשמות של משתנים:

// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};

עם ההגדרה הזו, חבילת Webpack לא תקבץ חבילות של react ו-react-dom. במקום זאת, הם יוחלפו במשהו כמו:

// bundle.js (part of)
(function(module, exports) {
  // A module that exports `window.React`. Without `externals`,
  // this module would include the whole React bundle
  module.exports = React;
}),
(function(module, exports) {
  // A module that exports `window.ReactDOM`. Without `externals`,
  // this module would include the whole ReactDOM bundle
  module.exports = ReactDOM;
})

אם יחסי התלות נטענות כחבילות AMD

אם הקוד שהוא לא חבילת האינטרנט לא חושף תלות ב-window, העניינים מורכבים יותר. עם זאת, עדיין תוכלו להימנע מטעינה של אותו קוד פעמיים אם הקוד שלא שייך ל-webpack צורך את יחסי התלות האלה כחבילות ADD.

כדי לעשות זאת, צריך להדר את קוד ה-webpack כחבילת AMD ומודולים חלופיים לכתובות ה-URL של הספרייה:

// webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'amd'
  },
  externals: {
    'react': {
      amd: '/libraries/react.min.js'
    },
    'react-dom': {
      amd: '/libraries/react-dom.min.js'
    }
  }
};

ה-Webpack יכסה את החבילה בתוך define() ויהיה תלוי בכתובות ה-URL האלה:

// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

אם קוד שאינו Webpack משתמש באותן כתובות URL כדי לטעון את יחסי התלות שלו, הקבצים האלה ייטענו רק פעם אחת – בקשות נוספות ישתמשו במטמון של הטוען.

קריאה נוספת

סיכום

  • מפעילים את מצב הייצור אם משתמשים ב-webpack 4
  • מזעור הקוד שלך באמצעות אפשרויות המזעור והטעינה ברמת החבילה
  • כדי להסיר את הקוד לפיתוח בלבד, מחליפים את NODE_ENV ב-production
  • שימוש במודולים של ES כדי לאפשר רעידת עצים
  • דחיסת תמונות
  • החלת אופטימיזציות ספציפיות לתלות
  • הפעלה של שרשור מודולים
  • כדאי להשתמש ב-externals אם זה מתאים לך