کاهش بارهای جاوا اسکریپت با تکان دادن درخت

برنامه های وب امروزی می توانند بسیار بزرگ شوند، به خصوص بخش جاوا اسکریپت آنها. از اواسط سال 2018، بایگانی HTTP اندازه متوسط ​​انتقال جاوا اسکریپت را در دستگاه های تلفن همراه تقریباً 350 کیلوبایت قرار می دهد. و این فقط اندازه انتقال است! جاوا اسکریپت اغلب هنگام ارسال از طریق شبکه فشرده می شود، به این معنی که مقدار واقعی جاوا اسکریپت پس از اینکه مرورگر آن را از حالت فشرده خارج کرد، کمی بیشتر می شود. اشاره به این مهم است، زیرا تا آنجا که به پردازش منابع مربوط می شود، فشرده سازی بی ربط است. 900 کیلوبایت جاوا اسکریپت از حالت فشرده خارج شده هنوز 900 کیلوبایت برای تجزیه کننده و کامپایلر است، حتی اگر زمانی که فشرده شود ممکن است تقریباً 300 کیلوبایت باشد.

نموداری که فرآیند دانلود، فشرده سازی، تجزیه، کامپایل و اجرای جاوا اسکریپت را نشان می دهد.
فرآیند دانلود و اجرای جاوا اسکریپت. توجه داشته باشید که اگرچه حجم انتقال اسکریپت فشرده 300 کیلوبایت است، اما هنوز جاوا اسکریپت 900 کیلوبایتی دارد که باید تجزیه، کامپایل و اجرا شود.

جاوا اسکریپت یک منبع گران قیمت برای پردازش است. بر خلاف تصاویری که پس از دانلود فقط زمان رمزگشایی نسبتاً کمی دارند، جاوا اسکریپت باید تجزیه، کامپایل و سپس در نهایت اجرا شود. بایت به بایت، این امر جاوا اسکریپت را نسبت به سایر انواع منابع گران تر می کند.

نموداری که زمان پردازش 170 کیلوبایت جاوا اسکریپت را با یک تصویر JPEG با اندازه معادل مقایسه می کند. منبع جاوا اسکریپت بایت برای بایت بسیار بیشتر از JPEG است.
هزینه پردازش تجزیه/کامپایل 170 کیلوبایت جاوا اسکریپت در مقابل زمان رمزگشایی یک JPEG با اندازه معادل. ( منبع ).

در حالی که به طور مداوم بهبودهایی برای بهبود کارایی موتورهای جاوا اسکریپت انجام می شود ، بهبود عملکرد جاوا اسکریپت - مثل همیشه - وظیفه ای برای توسعه دهندگان است.

برای این منظور، تکنیک هایی برای بهبود عملکرد جاوا اسکریپت وجود دارد. تقسیم کد ، یکی از این تکنیک‌ها است که با پارتیشن‌بندی جاوا اسکریپت برنامه به تکه‌ها، و ارائه آن تکه‌ها به مسیرهای برنامه‌ای که به آن‌ها نیاز دارند، عملکرد را بهبود می‌بخشد.

در حالی که این تکنیک کار می‌کند، مشکل رایج برنامه‌های کاربردی سنگین جاوا اسکریپت، که شامل کدهایی است که هرگز استفاده نمی‌شوند را برطرف نمی‌کند. تکان دادن درخت برای حل این مشکل تلاش می کند.

تکان درخت چیست؟

تکان دادن درخت نوعی حذف کد مرده است. این اصطلاح توسط Rollup رایج شد ، اما مفهوم حذف کد مرده مدتی است که وجود داشته است. این مفهوم همچنین در وب پک خرید پیدا کرده است که در این مقاله از طریق یک برنامه نمونه نشان داده شده است.

اصطلاح "تکان درخت" از مدل ذهنی برنامه شما و وابستگی های آن به عنوان یک ساختار درخت مانند می آید. هر گره در درخت نشان دهنده یک وابستگی است که عملکرد مجزایی را برای برنامه شما فراهم می کند. در برنامه‌های مدرن، این وابستگی‌ها از طریق عبارت‌های import ثابت مانند زیر وارد می‌شوند:

// Import all the array utilities!
import arrayUtils from "array-utils";

وقتی یک برنامه جوان است - اگر بخواهید یک نهال - ممکن است وابستگی کمی داشته باشد. همچنین از بیشتر – اگر نه همه – وابستگی هایی که اضافه می کنید استفاده می کند. با این حال، همانطور که برنامه شما بالغ می شود، وابستگی های بیشتری می توانند اضافه شوند. برای مسائل مرکب، وابستگی‌های قدیمی‌تر از دسترس خارج می‌شوند، اما ممکن است از پایگاه کد شما حذف نشوند. نتیجه نهایی این است که یک برنامه در نهایت با جاوا اسکریپت استفاده نشده زیادی ارسال می شود. درخت تکان دادن با بهره گیری از نحوه کشش عبارات import ایستا در بخش های خاصی از ماژول های ES6 این مشکل را برطرف می کند:

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";

تفاوت بین این نمونه import و نمونه قبلی در این است که به جای وارد کردن همه چیز از ماژول "array-utils" - که می تواند کد زیادی باشد) - این مثال فقط بخش های خاصی از آن را وارد می کند. در ساخت‌های توسعه‌دهنده، این چیزی را تغییر نمی‌دهد، زیرا کل ماژول بدون در نظر گرفتن وارد می‌شود. در ساخت‌های تولیدی، وب پک می‌تواند به گونه‌ای پیکربندی شود که صادرات ماژول‌های ES6 را که به‌صراحت وارد نشده‌اند، از بین ببرد و این بیلدهای تولیدی را کوچک‌تر کند. در این راهنما، یاد خواهید گرفت که چگونه این کار را انجام دهید!

یافتن فرصت هایی برای تکان دادن درخت

برای اهداف توضیحی، یک نمونه برنامه یک صفحه ای موجود است که نشان می دهد تکان دادن درخت چگونه کار می کند. می‌توانید آن را شبیه‌سازی کنید و در صورت تمایل دنبال کنید، اما ما در این راهنما همه مراحل را با هم پوشش خواهیم داد، بنابراین شبیه‌سازی ضروری نیست (مگر اینکه یادگیری عملی کار شما باشد).

برنامه نمونه یک پایگاه داده قابل جستجو از پدال های جلوه گیتار است. شما یک پرس و جو وارد می کنید و لیستی از پدال های افکت ظاهر می شود.

اسکرین شات از نمونه برنامه یک صفحه ای برای جستجو در پایگاه داده پدال های افکت گیتار.
تصویری از برنامه نمونه.

رفتاری که این برنامه را هدایت می‌کند به فروشنده (به عنوان مثال، Preact و Emotion ) و بسته‌های کد خاص برنامه (یا «تکه‌ها»، همانطور که پک وب آنها را می‌نامد، تفکیک می‌شود:

تصویری از دو بسته کد برنامه (یا تکه‌ها) که در پانل شبکه DevTools Chrome نشان داده شده است.
دو بسته جاوا اسکریپت برنامه. اینها اندازه های فشرده نشده هستند.

بسته‌های جاوا اسکریپت که در شکل بالا نشان داده شده‌اند، ساخت‌های تولیدی هستند، به این معنی که از طریق uglification بهینه شده‌اند. 21.1 کیلوبایت برای یک بسته خاص برنامه بد نیست، اما باید توجه داشت که هیچ تکانی درختی رخ نمی دهد. بیایید به کد برنامه نگاه کنیم و ببینیم برای رفع آن چه کاری می توان انجام داد.

در هر برنامه‌ای، یافتن فرصت‌های تکان دادن درخت مستلزم جستجوی عبارات import ثابت است. در نزدیکی بالای فایل کامپوننت اصلی ، خطی مانند این را خواهید دید:

import * as utils from "../../utils/utils";

می‌توانید ماژول‌های ES6 را به روش‌های مختلفی وارد کنید ، اما مواردی مانند این باید توجه شما را جلب کنند. این خط خاص می گوید " همه چیز را از ماژول utils import و آن را در فضای نامی به نام utils قرار دهید." سوال بزرگی که در اینجا می توان پرسید این است که "در آن ماژول چقدر چیز وجود دارد؟"

اگر به کد منبع ماژول utils نگاه کنید، متوجه خواهید شد که حدود 1300 خط کد وجود دارد.

آیا به این همه چیز نیاز دارید؟ بیایید با جستجوی فایل مؤلفه اصلی که ماژول utils را وارد می‌کند دوباره بررسی کنیم تا ببینیم چند نمونه از آن فضای نام ظاهر می‌شود.

اسکرین شات از جستجو در ویرایشگر متن برای 'utils.'، که تنها 3 نتیجه را برمی گرداند.
فضای نام utils که هزاران ماژول را از آن وارد کرده‌ایم، تنها سه بار در فایل کامپوننت اصلی فراخوانی می‌شود.

همانطور که مشخص است، فضای نام utils تنها در سه نقطه در برنامه ما ظاهر می شود - اما برای چه توابعی؟ اگر دوباره به فایل مؤلفه اصلی نگاهی بیندازید، به نظر می‌رسد که تنها یک تابع است که utils.simpleSort است که برای مرتب‌سازی فهرست نتایج جستجو بر اساس تعدادی معیار هنگام تغییر فهرست‌های کشویی مرتب‌سازی استفاده می‌شود:

if (this.state.sortBy === "model") {
  // `simpleSort` gets used here...
  json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  // ..and here...
  json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
  // ..and here.
  json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}

از یک فایل 1300 خطی با انبوه صادرات، فقط یکی از آنها استفاده می شود. این منجر به ارسال تعداد زیادی جاوا اسکریپت استفاده نشده می شود.

اگرچه این برنامه نمونه مسلماً کمی ساختگی است، اما این واقعیت را تغییر نمی‌دهد که این نوع سناریوی مصنوعی شبیه فرصت‌های بهینه‌سازی واقعی است که ممکن است در یک برنامه وب تولیدی با آن مواجه شوید. اکنون که فرصتی را برای مفید بودن تکان دادن درخت شناسایی کرده اید، واقعاً چگونه انجام می شود؟

حفظ Babel از انتقال ماژول های ES6 به ماژول های CommonJS

بابل ابزاری ضروری است، اما ممکن است مشاهده اثرات تکان دادن درختان را کمی دشوارتر کند. اگر از @babel/preset-env استفاده می‌کنید، Babel ممکن است ماژول‌های ES6 را به ماژول‌های CommonJS سازگارتر تبدیل کند—یعنی ماژول‌هایی که به جای import require دارید.

از آنجایی که تکان دادن درخت برای ماژول‌های CommonJS دشوارتر است، وب‌پک نمی‌داند چه چیزی را از باندل‌ها هرس کند اگر تصمیم به استفاده از آن‌ها داشته باشید. راه حل این است که @babel/preset-env را پیکربندی کنید تا به صراحت ماژول های ES6 را به حال خود رها کنید. هر جا Babel را پیکربندی کنید - چه در babel.config.js یا package.json - این شامل اضافه کردن یک چیز اضافی است:

// babel.config.js
export default {
  presets: [
    [
      "@babel/preset-env", {
        modules: false
      }
    ]
  ]
}

مشخص کردن modules: false در پیکربندی @babel/preset-env شما باعث می‌شود Babel مطابق دلخواه رفتار کند، که به وب‌پک اجازه می‌دهد درخت وابستگی شما را تجزیه و تحلیل کند و وابستگی‌های استفاده نشده را از بین ببرد.

در نظر گرفتن عوارض جانبی

یکی دیگر از جنبه هایی که باید در هنگام از بین بردن وابستگی ها از برنامه خود در نظر بگیرید این است که آیا ماژول های پروژه شما دارای عوارض جانبی هستند یا خیر. یک مثال از یک عارضه جانبی زمانی است که یک تابع چیزی خارج از محدوده خود را تغییر می دهد، که یکی از عوارض جانبی اجرای آن است:

let fruits = ["apple", "orange", "pear"];

console.log(fruits); // (3) ["apple", "orange", "pear"]

const addFruit = function(fruit) {
  fruits.push(fruit);
};

addFruit("kiwi");

console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]

در این مثال، addFruit هنگامی که آرایه fruits را اصلاح می‌کند، یک عارضه جانبی ایجاد می‌کند که خارج از محدوده آن است.

عوارض جانبی برای ماژول‌های ES6 نیز اعمال می‌شود و این در زمینه تکان دادن درخت اهمیت دارد. ماژول‌هایی که ورودی‌های قابل پیش‌بینی را دریافت می‌کنند و خروجی‌های قابل پیش‌بینی یکسانی را بدون تغییر چیزی خارج از محدوده خود تولید می‌کنند، وابستگی‌هایی هستند که اگر از آن‌ها استفاده نکنیم، می‌توان با خیال راحت کنار گذاشت. آنها تکه های کد ماژولار و مستقلی هستند. از این رو، "ماژول".

در مورد بسته وب، می‌توان از یک اشاره برای تعیین اینکه بسته و وابستگی‌های آن عاری از عوارض جانبی هستند، با مشخص کردن "sideEffects": false در فایل package.json پروژه استفاده کرد:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": false
}

از طرف دیگر، می‌توانید به وب‌پک بگویید که کدام فایل‌های خاص بدون عوارض جانبی نیستند:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": [
    "./src/utils/utils.js"
  ]
}

در مثال اخیر، هر فایلی که مشخص نشده باشد، فاقد عوارض جانبی فرض می شود. اگر نمی‌خواهید این را به فایل package.json خود اضافه کنید، می‌توانید این پرچم را در پیکربندی بسته وب خود از طریق module.rules نیز مشخص کنید .

واردات فقط موارد مورد نیاز

پس از دستور به Babel برای رها کردن ماژول‌های ES6، یک تنظیم جزئی در نحو import ما لازم است تا فقط توابع مورد نیاز ماژول utils را وارد کنیم. در مثال این راهنما، تنها چیزی که نیاز است تابع simpleSort است:

import { simpleSort } from "../../utils/utils";

از آنجا که فقط simpleSort به جای کل ماژول utils وارد می شود، هر نمونه از utils.simpleSort باید به simpleSort تغییر کند:

if (this.state.sortBy === "model") {
  json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  json = simpleSort(json, "type", this.state.sortOrder);
} else {
  json = simpleSort(json, "manufacturer", this.state.sortOrder);
}

این باید تمام چیزی باشد که برای تکان دادن درخت در این مثال لازم است. این خروجی بسته وب قبل از تکان دادن درخت وابستگی است:

                 Asset      Size  Chunks             Chunk Names
js/vendors.16262743.js  37.1 KiB       0  [emitted]  vendors
   js/main.797ebb8b.js  20.8 KiB       1  [emitted]  main

این خروجی پس از موفقیت آمیز بودن تکان دادن درخت است:

                 Asset      Size  Chunks             Chunk Names
js/vendors.45ce9b64.js  36.9 KiB       0  [emitted]  vendors
   js/main.559652be.js  8.46 KiB       1  [emitted]  main

در حالی که هر دو بسته کوچک شده اند، این در واقع بسته main است که بیشترین سود را دارد. با تکان دادن قسمت های استفاده نشده ماژول utils ، بسته main حدود 60٪ کوچک می شود. این نه تنها مدت زمان دانلود اسکریپت را کاهش می دهد، بلکه زمان پردازش را نیز کاهش می دهد.

برو چند درخت را تکان بده!

هر مسافت پیموده شده ای که از تکان دادن درختان بدست می آورید به برنامه شما و وابستگی ها و معماری آن بستگی دارد. آن را امتحان کنید! اگر به درستی می‌دانید که بسته‌کننده ماژول خود را برای انجام این بهینه‌سازی تنظیم نکرده‌اید، هیچ ضرری ندارد تلاش کنید و ببینید که چگونه برنامه شما به نفع آن است.

ممکن است از تکان دادن درختان به افزایش عملکرد قابل توجهی پی ببرید، یا اصلاً زیاد نباشد. اما با پیکربندی سیستم ساخت خود برای استفاده از این بهینه‌سازی در ساخت‌های تولیدی و وارد کردن انتخابی تنها آنچه برنامه شما نیاز دارد، به طور فعال بسته‌های برنامه خود را تا حد امکان کوچک نگه می‌دارید.

تشکر ویژه از کریستوفر باکستر، جیسون میلر ، آدی عثمانی ، جف پوسنیک ، سم ساکون و فیلیپ والتون برای بازخورد ارزشمندشان که کیفیت این مقاله را به طور قابل توجهی بهبود بخشید.