গাছ কাঁপানোর সাথে জাভাস্ক্রিপ্ট পেলোড কমিয়ে দিন

আজকের ওয়েব অ্যাপ্লিকেশনগুলো বেশ বড় হতে পারে, বিশেষ করে সেগুলোর জাভাস্ক্রিপ্ট অংশটি। ২০১৮ সালের মাঝামাঝি সময়ের তথ্য অনুযায়ী, এইচটিটিপি আর্কাইভের মতে মোবাইল ডিভাইসে জাভাস্ক্রিপ্টের গড় ট্রান্সফার সাইজ ছিল প্রায় ৩৫০ কিলোবাইট। আর এটা শুধুমাত্র ট্রান্সফার সাইজ! নেটওয়ার্কের মাধ্যমে পাঠানোর সময় জাভাস্ক্রিপ্ট প্রায়শই কম্প্রেস করা থাকে, যার মানে হলো ব্রাউজার ডিকম্প্রেস করার পর জাভাস্ক্রিপ্টের প্রকৃত পরিমাণ আরও অনেক বেশি হয়ে যায়। এই বিষয়টি উল্লেখ করা জরুরি, কারণ রিসোর্স প্রসেসিংয়ের ক্ষেত্রে কম্প্রেশন অপ্রাসঙ্গিক। কম্প্রেস করা অবস্থায় প্রায় ৩০০ কিলোবাইট হলেও, ডিকম্প্রেস করা ৯০০ কিলোবাইট জাভাস্ক্রিপ্ট পার্সার এবং কম্পাইলারের কাছে ৯০০ কিলোবাইটই থাকে।

জাভাস্ক্রিপ্ট ডাউনলোড, ডিকম্প্রেস, পার্স, কম্পাইল এবং এক্সিকিউট করার প্রক্রিয়া চিত্রিত একটি ডায়াগ্রাম।
জাভাস্ক্রিপ্ট ডাউনলোড এবং চালানোর প্রক্রিয়া। উল্লেখ্য যে, যদিও স্ক্রিপ্টটির সংকুচিত আকার ৩০০ কিলোবাইট, তবুও এটি ৯০০ কিলোবাইট জাভাস্ক্রিপ্ট যা পার্স, কম্পাইল এবং এক্সিকিউট করতে হয়।

জাভাস্ক্রিপ্ট প্রসেস করা একটি ব্যয়বহুল রিসোর্স। ছবির মতো নয়, যা একবার ডাউনলোড হয়ে গেলে ডিকোড করতে তুলনামূলকভাবে সামান্য সময় লাগে, জাভাস্ক্রিপ্টকে প্রথমে পার্স, তারপর কম্পাইল এবং সবশেষে এক্সিকিউট করতে হয়। বাইটের হিসাবে, এই কারণে জাভাস্ক্রিপ্ট অন্যান্য ধরনের রিসোর্সের চেয়ে বেশি ব্যয়বহুল।

১৭০ কিলোবাইট জাভাস্ক্রিপ্ট এবং সমপরিমাণ আকারের একটি জেপিইজি ছবির প্রসেসিং সময়ের তুলনামূলক একটি ডায়াগ্রাম। প্রতি বাইটের হিসাবে জাভাস্ক্রিপ্ট রিসোর্সটি জেপিইজি-র চেয়ে অনেক বেশি রিসোর্স-নিবিড়।
১৭০ কিলোবাইট জাভাস্ক্রিপ্ট পার্সিং/কম্পাইল করার প্রসেসিং খরচ বনাম সম আকারের একটি জেপিইজি ফাইল ডিকোড করার সময়। ( উৎস )

যদিও জাভাস্ক্রিপ্ট ইঞ্জিনগুলোর কার্যকারিতা বাড়ানোর জন্য ক্রমাগত উন্নতি করা হচ্ছে , জাভাস্ক্রিপ্টের পারফরম্যান্স উন্নত করাটা বরাবরের মতোই ডেভেলপারদেরই একটি কাজ।

এই লক্ষ্যে, জাভাস্ক্রিপ্টের পারফরম্যান্স উন্নত করার বিভিন্ন কৌশল রয়েছে। কোড স্প্লিটিং এমনই একটি কৌশল, যা অ্যাপ্লিকেশনের জাভাস্ক্রিপ্টকে বিভিন্ন খণ্ডে বিভক্ত করে এবং শুধুমাত্র অ্যাপ্লিকেশনের সেইসব রুটে সেই খণ্ডগুলো সরবরাহ করে পারফরম্যান্স উন্নত করে, যেগুলোর সেগুলোর প্রয়োজন হয়।

যদিও এই কৌশলটি কাজ করে, এটি জাভাস্ক্রিপ্ট-নির্ভর অ্যাপ্লিকেশনগুলির একটি সাধারণ সমস্যার সমাধান করে না, আর তা হলো এমন কোডের অন্তর্ভুক্তি যা কখনও ব্যবহৃত হয় না। ট্রি শেকিং এই সমস্যাটি সমাধান করার চেষ্টা করে।

গাছ কাঁপানো বলতে কী বোঝায়?

ট্রি শেকিং হলো ডেড কোড এলিমিনেশনের একটি রূপ। এই পরিভাষাটি রোলআপ (Rollup) দ্বারা জনপ্রিয় হয়েছিল , কিন্তু ডেড কোড এলিমিনেশনের ধারণাটি বেশ কিছু সময় ধরেই বিদ্যমান। এই ধারণাটি ওয়েবপ্যাক (webpack) -এও স্থান পেয়েছে, যা এই নিবন্ধে একটি নমুনা অ্যাপের মাধ্যমে প্রদর্শন করা হয়েছে।

"ট্রি শেকিং" পরিভাষাটি এসেছে আপনার অ্যাপ্লিকেশন এবং এর নির্ভরতাগুলোকে একটি বৃক্ষ-সদৃশ কাঠামো হিসেবে দেখার মানসিক মডেল থেকে। এই বৃক্ষের প্রতিটি নোড একটি নির্ভরতার প্রতিনিধিত্ব করে, যা আপনার অ্যাপের জন্য স্বতন্ত্র কার্যকারিতা প্রদান করে। আধুনিক অ্যাপগুলোতে, এই নির্ভরতাগুলো স্ট্যাটিক import স্টেটমেন্টের মাধ্যমে নিম্নোক্তভাবে যুক্ত করা হয়:

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

যখন একটি অ্যাপ নতুন থাকে—এক প্রকার চারাগাছ—তখন এর খুব কম ডিপেন্ডেন্সি থাকতে পারে। এটি আপনার যোগ করা প্রায় সমস্ত ডিপেন্ডেন্সি ব্যবহার করে থাকে। কিন্তু, আপনার অ্যাপটি পরিপক্ক হওয়ার সাথে সাথে আরও ডিপেন্ডেন্সি যুক্ত হতে পারে। পরিস্থিতি আরও জটিল হয় যখন পুরোনো ডিপেন্ডেন্সিগুলো ব্যবহারের বাইরে চলে যায়, কিন্তু আপনার কোডবেস থেকে সেগুলো ছেঁটে ফেলা নাও হতে পারে। এর ফলে, একটি অ্যাপে প্রচুর অব্যবহৃত জাভাস্ক্রিপ্ট থেকে যায়। ট্রি শেকিং (Tree shaking) এই সমস্যার সমাধান করে, যা স্ট্যাটিক import স্টেটমেন্টগুলো কীভাবে ES6 মডিউলের নির্দিষ্ট অংশগুলোকে নিয়ে আসে, তার সুবিধা গ্রহণ করে।

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

এই import উদাহরণ এবং আগেরটির মধ্যে পার্থক্য হলো "array-utils" মডিউলের সবকিছু ইম্পোর্ট করার পরিবর্তে—যা অনেক বেশি কোড হতে পারে—এই উদাহরণটি শুধুমাত্র এর নির্দিষ্ট কিছু অংশ ইম্পোর্ট করে। ডেভ বিল্ডে এটি কোনো পরিবর্তন আনে না, কারণ সেক্ষেত্রে পুরো মডিউলটিই ইম্পোর্ট হয়ে যায়। প্রোডাকশন বিল্ডে, ওয়েবপ্যাককে এমনভাবে কনফিগার করা যায় যাতে এটি স্পষ্টভাবে ইম্পোর্ট না করা ES6 মডিউলগুলোর এক্সপোর্টগুলো বাদ দিয়ে দেয়, যার ফলে সেই প্রোডাকশন বিল্ডগুলো আকারে ছোট হয়। এই গাইডে, আপনি শিখবেন কীভাবে ঠিক এই কাজটি করতে হয়!

গাছ নাড়ানোর সুযোগ খোঁজা

উদাহরণস্বরূপ, ট্রি শেকিং কীভাবে কাজ করে তা দেখানোর জন্য একটি এক-পৃষ্ঠার নমুনা অ্যাপ রয়েছে। আপনি চাইলে এটি ক্লোন করে অনুসরণ করতে পারেন, কিন্তু আমরা এই গাইডে প্রতিটি ধাপ একসাথে আলোচনা করব, তাই ক্লোন করার প্রয়োজন নেই (যদি না হাতে-কলমে শেখা আপনার পছন্দের বিষয় হয়)।

স্যাম্পল অ্যাপটি হলো গিটার এফেক্ট পেডালের একটি অনুসন্ধানযোগ্য ডেটাবেস। আপনি একটি কোয়েরি লিখলে এফেক্ট পেডালগুলোর একটি তালিকা প্রদর্শিত হবে।

গিটার এফেক্ট পেডালের একটি ডাটাবেস অনুসন্ধানের জন্য তৈরি এক পৃষ্ঠার একটি নমুনা অ্যাপ্লিকেশনের স্ক্রিনশট।
নমুনা অ্যাপটির একটি স্ক্রিনশট।

এই অ্যাপটিকে চালনা করে এমন আচরণকে ভেন্ডর (যেমন, Preact এবং Emotion ) এবং অ্যাপ-নির্দিষ্ট কোড বান্ডেল (বা "চাঙ্ক", যেমনটি webpack বলে) -এ বিভক্ত করা হয়েছে:

ক্রোমের ডেভটুলস-এর নেটওয়ার্ক প্যানেলে প্রদর্শিত দুটি অ্যাপ্লিকেশন কোড বান্ডেল (বা চাঙ্ক)-এর একটি স্ক্রিনশট।
অ্যাপটির দুটি জাভাস্ক্রিপ্ট বান্ডেল। এগুলো অসংকুচিত আকার।

উপরের চিত্রে দেখানো জাভাস্ক্রিপ্ট বান্ডেলগুলো হলো প্রোডাকশন বিল্ড, অর্থাৎ এগুলো আগলিফিকেশনের মাধ্যমে অপ্টিমাইজ করা হয়েছে। একটি অ্যাপ-নির্দিষ্ট বান্ডেলের জন্য ২১.১ কেবি খুব বেশি নয়, কিন্তু এটি লক্ষণীয় যে এখানে কোনো ট্রি শেকিং হচ্ছে না। চলুন অ্যাপের কোডটি দেখে নেওয়া যাক এবং এটি ঠিক করার জন্য কী করা যেতে পারে তা খতিয়ে দেখা যাক।

যেকোনো অ্যাপ্লিকেশনে, ট্রি শেকিং-এর সুযোগ খুঁজে বের করার জন্য স্ট্যাটিক import স্টেটমেন্টগুলো খুঁজতে হবে। মূল কম্পোনেন্ট ফাইলের উপরের দিকে , আপনি এই ধরনের একটি লাইন দেখতে পাবেন:

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

আপনি বিভিন্ন উপায়ে ES6 মডিউল ইম্পোর্ট করতে পারেন, কিন্তু এই ধরনের বিষয়গুলো আপনার দৃষ্টি আকর্ষণ করবে। এই নির্দিষ্ট লাইনটিতে বলা হয়েছে, " utils মডিউল থেকে সবকিছু import utils নামের একটি নেমস্পেসে রাখো।" এখানে বড় প্রশ্নটি হলো, "ঐ মডিউলটিতে ঠিক কতটা জিনিস রয়েছে?"

আপনি যদি utils মডিউলের সোর্স কোড দেখেন, তাহলে দেখতে পাবেন সেখানে প্রায় ১,৩০০ লাইনের কোড রয়েছে।

আপনার কি এই সবকিছুর প্রয়োজন আছে ? চলুন, utils মডিউলটি ইম্পোর্ট করা মূল কম্পোনেন্ট ফাইলটিতে সার্চ করে নিশ্চিত হয়ে নিই যে ওই নেমস্পেসটির কতগুলো ইনস্ট্যান্স পাওয়া যায়।

টেক্সট এডিটরে 'utils.' লিখে সার্চ করার একটি স্ক্রিনশট, যেখানে মাত্র ৩টি ফলাফল দেখাচ্ছে।
আমরা যে 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);
}

অনেকগুলো এক্সপোর্টসহ ১,৩০০ লাইনের একটি ফাইলের মধ্যে মাত্র একটি ব্যবহৃত হয়। এর ফলে প্রচুর অব্যবহৃত জাভাস্ক্রিপ্ট ফাইল হিসেবে থেকে যায়।

যদিও এই উদাহরণ অ্যাপটি কিছুটা কৃত্রিম, তবুও এই সত্যটি বদলায় না যে এই ধরনের কৃত্রিম পরিস্থিতি একটি প্রোডাকশন ওয়েব অ্যাপে আপনি যে প্রকৃত অপটিমাইজেশনের সুযোগগুলোর সম্মুখীন হতে পারেন, তারই অনুরূপ। এখন যেহেতু আপনি ট্রি শেকিংকে কার্যকর করার একটি সুযোগ চিহ্নিত করেছেন, তাহলে এটি আসলে কীভাবে করা হয়?

Babel-কে ES6 মডিউলগুলোকে CommonJS মডিউলে ট্রান্সপাইল করা থেকে বিরত রাখা

Babel একটি অপরিহার্য টুল, কিন্তু এটি ট্রি শেকিং-এর প্রভাব পর্যবেক্ষণ করাকে কিছুটা কঠিন করে তুলতে পারে। আপনি যদি @babel/preset-env ব্যবহার করেন, তাহলে Babel ES6 মডিউলগুলোকে আরও ব্যাপকভাবে সামঞ্জস্যপূর্ণ CommonJS মডিউলে রূপান্তরিত করতে পারে —অর্থাৎ, এমন মডিউল যা আপনি import এর পরিবর্তে require

যেহেতু CommonJS মডিউলগুলির জন্য ট্রি শেকিং করা আরও কঠিন, তাই আপনি যদি সেগুলি ব্যবহার করার সিদ্ধান্ত নেন, তাহলে webpack বুঝতে পারবে না যে বান্ডলগুলি থেকে কী ছেঁটে ফেলতে হবে। এর সমাধান হলো @babel/preset-env কনফিগার করে ES6 মডিউলগুলিকে স্পষ্টভাবে অপরিবর্তিত রাখা। আপনি Babel যেখানেই কনফিগার করুন না কেন—তা babel.config.js এ হোক বা package.json —এর জন্য আপনাকে সামান্য কিছু অতিরিক্ত যোগ করতে হবে:

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

আপনার @babel/preset-env কনফিগে modules: false উল্লেখ করলে Babel কাঙ্ক্ষিতভাবে কাজ করে, যা webpack-কে আপনার ডিপেন্ডেন্সি ট্রি বিশ্লেষণ করতে এবং অব্যবহৃত ডিপেন্ডেন্সিগুলো বাদ দিতে সাহায্য করে।

পার্শ্ব প্রতিক্রিয়া মাথায় রেখে

আপনার অ্যাপ থেকে ডিপেন্ডেন্সি অপসারণ করার সময় বিবেচনা করার মতো আরেকটি বিষয় হলো, আপনার প্রোজেক্টের মডিউলগুলোর কোনো সাইড ইফেক্ট আছে কিনা। সাইড ইফেক্টের একটি উদাহরণ হলো যখন কোনো ফাংশন তার নিজের স্কোপের বাইরের কিছু পরিবর্তন করে, যা তার এক্সিকিউশনের একটি সাইড ইফেক্ট

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 মডিউলের ক্ষেত্রেও প্রযোজ্য, এবং ট্রি শেকিং-এর প্রেক্ষাপটে এটি একটি গুরুত্বপূর্ণ বিষয়। যে মডিউলগুলো নিজেদের স্কোপের বাইরের কোনো কিছু পরিবর্তন না করেই অনুমানযোগ্য ইনপুট গ্রহণ করে এবং একইভাবে অনুমানযোগ্য আউটপুট তৈরি করে, সেগুলো হলো ডিপেন্ডেন্সি, যা ব্যবহার না করলে নিরাপদে বাদ দেওয়া যায়। এগুলো হলো স্বয়ংসম্পূর্ণ, মডিউলার কোডের অংশ। একারণেই এদেরকে "মডিউল" বলা হয়।

webpack-এর ক্ষেত্রে, কোনো প্রোজেক্টের package.json ফাইলে "sideEffects": false উল্লেখ করে একটি প্যাকেজ এবং তার ডিপেন্ডেন্সিগুলো যে সাইড এফেক্ট-মুক্ত, তা নির্দিষ্ট করার জন্য একটি হিন্ট ব্যবহার করা যেতে পারে।

{
  "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 এর মাধ্যমে আপনার webpack কনফিগেও এই ফ্ল্যাগটি নির্দিষ্ট করে দিতে পারেন

শুধুমাত্র প্রয়োজনীয় জিনিস আমদানি করা

Babel-কে ES6 মডিউলগুলো অপরিবর্তিত রাখতে নির্দেশ দেওয়ার পর, utils মডিউল থেকে শুধু প্রয়োজনীয় ফাংশনগুলো আনার জন্য আমাদের import সিনট্যাক্সে সামান্য পরিবর্তন আনতে হবে। এই গাইডের উদাহরণে, শুধুমাত্র simpleSort ফাংশনটিই প্রয়োজন:

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

যেহেতু সম্পূর্ণ utils মডিউলের পরিবর্তে শুধুমাত্র simpleSort ইম্পোর্ট করা হচ্ছে, তাই 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 বান্ডেলটি প্রায় ৬০% ছোট হয়ে যায়। এর ফলে শুধু স্ক্রিপ্টটির ডাউনলোডের সময়ই কমে না, প্রসেসিংয়ের সময়ও কমে আসে।

যাও, গিয়ে কিছু গাছ ঝাঁকাও!

ট্রি শেকিং থেকে আপনি কতটা সুবিধা পাবেন, তা আপনার অ্যাপ, এর ডিপেন্ডেন্সি এবং আর্কিটেকচারের উপর নির্ভর করে। চেষ্টা করে দেখুন! যদি আপনি নিশ্চিতভাবে জানেন যে এই অপটিমাইজেশনটি করার জন্য আপনার মডিউল বান্ডলার সেট আপ করা নেই, তবে এটি চেষ্টা করে দেখতে এবং আপনার অ্যাপ্লিকেশনের জন্য কীভাবে উপকারী তা জানতে কোনো ক্ষতি নেই।

ট্রি শেকিং থেকে আপনি পারফরম্যান্সে উল্লেখযোগ্য উন্নতি দেখতে পারেন, অথবা তেমন কিছুই নাও পেতে পারেন। কিন্তু প্রোডাকশন বিল্ডে এই অপটিমাইজেশনের সুবিধা নিতে আপনার বিল্ড সিস্টেমকে কনফিগার করে এবং আপনার অ্যাপ্লিকেশনের জন্য শুধু প্রয়োজনীয় অংশগুলো বেছে বেছে ইম্পোর্ট করার মাধ্যমে, আপনি সক্রিয়ভাবে আপনার অ্যাপ্লিকেশন বান্ডেলগুলোকে যথাসম্ভব ছোট রাখতে পারবেন।

ক্রিস্টোফার ব্যাক্সটার, জেসন মিলার , অ্যাডি ওসমানি , জেফ পসনিক , স্যাম স্যাকোন এবং ফিলিপ ওয়ালটনকে তাদের মূল্যবান মতামতের জন্য বিশেষ ধন্যবাদ, যা এই নিবন্ধটির মানকে উল্লেখযোগ্যভাবে উন্নত করেছে।