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

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

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

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

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

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

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

קריאה נוספת

הפעלת הקטנה

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

// 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 יש תמיכה בשתי דרכים להקטנת הקוד: הקטנה ברמת החבילה, אפשרויות ספציפיות ל-loader. יש להשתמש בהם בו-זמנית.

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

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

  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(),
  ],
};

אפשרויות ספציפיות ל-Loader

הדרך השנייה להקטנת הקוד היא אפשרויות ספציפיות לגורם הטעינה (מה ). בעזרת אפשרויות הטעינה אפשר לדחוס נתונים הממוזער לא יכול להקטין. לדוגמה, כשמייבאים קובץ 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}",""]);

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

// 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.');
}
// …

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

// 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.'
);
// …

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

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

ב-webpack 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. כל המופעים של process.env.NODE_ENV יוחלפו ב-Webpack "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' תמיד שגוי, והפלאגין מבין שהקוד שבתוך ההסתעפויות האלה אף פעם לא יפעל:

    // 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 יכול לבצע רעידת עצים. רעידת עצים מתרחשת כאשר באקראי שחוצה את כל עץ התלות, בודקת באילו יחסי תלות נמצאים בשימוש ומסיר את יחסי התלות שלא נמצאים בשימוש. אז, אם משתמשים בתחביר של מודול 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 כדי לבצע אופטימיזציה שלהם ו-webpack.

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 של קוד מוקטן לא עושות כלום.

דוגמה נוספת: Moment.js. גרסת 2.19.1 שלו לוקחת 223KB של קוד מוקטן, וזה ענק – הגודל הממוצע של JavaScript בדף היה 452KB באוקטובר 2017. עם זאת, הגודל הזה הוא 170KB. היא התאמה לשוק המקומי . אם המיקום אם לא משתמשים ב-Moment.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 הוסר. החבילה כוללת פחות מודולים – ופחות תקורת מודול!

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

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

ב-webpack 3 משתמשים ב-ModuleConcatenationPlugin:

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

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

קריאה נוספת

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

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

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

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

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

אם הקוד שהוא לא חבילת אינטרנט מסתמך על יחסי תלות שזמינים כמשתנים ב-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

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

לשם כך, צריך להדר את הקוד של חבילת ה-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 כדי לטעון את יחסי התלות, הקבצים האלה ייטענו רק פעם אחת – בקשות נוספות ישתמשו במטמון של ה-Loader.

קריאה נוספת

סיכום

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