17نکته درمورد کارایی برنامه نویسی JavaScript / node.js برای سرعت بخشیدن به برنامه ها

یکشنبه 6 خرداد 1397

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

17نکته درمورد کارایی برنامه نویسی JavaScript / node.js برای سرعت بخشیدن به برنامه ها

این مقاله در مورد dev-ops نیست و درباره مسائلی مثل فشرده‌سازی فایل ها یا راه اندازی redis  یا استفاده از docker و kubernetes برای اجرای برنامه بحث نمی کند. این مقاله در مورد برنامه نویسی در جاوااسکریپت جهت بهبود عملکرد برنامه است.

در صورتی که به یادگیری اصولی و حرفه ای این تکنولوژی قدرتمند علاقمند هستید میتوانید دوره کامل و جامع آموزش Node Js موجود در سایت تاپ لرن را مشاهده کنید .

ما در مورد جاوااسکریپت زیاد صحبت می کنیم، اما بعضی نکات فقط در مورد node.js هستند، یا ممکن است فقط در مورد جاوااسکریپت سمت کلاینت باشند. با این حال، از آنجا که امروزه اکثر توسعه دهندگان جاوااسکریپت  full-stack هستند (توسعه دهنده ای که در بیشتر تکنولوژی ها حرفی برای گفتن دارد)، ما فرض می کنیم که شما این موارد را به راحتی درک می کنید.

لطفا متغیرهای سراسری تعریف نکنید

این یکی از نکات بسیار آسان است. در حال حاضر افراد بسیاری با این نکته آشنا هستند، اما تعداد کمی دلیل آن را می دانند.

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

بنابراین هر بار که یک متغیر سراسری را فراخوانی می کنید، از میان تمام محدوده درخت (یا AST) عبور می کند تا پیدا شود که این امر به صرفه نیست.

نکته دوم این است که متغیرهای سراسری توسط garbage collector (بازیافت حافظه) پاک نمی شوند. بنابراین به طور مداوم متغیرهای سراسری بیشتری اضافه نمی کنیم.

فقط از آرایه های هم جنس استفاده کنید

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

کدهای متداول را در یک تابع قرار دهید

قبل از اینکه این مطلب را بیان کنیم، اجازه دهید کمی در مورد کامپایلرهای درجا (Just In Time compiler) صحبت کنیم.

این کامپایلرها درون V8 و دیگر موتورهای جاوااسکریپت برای بهینه سازی hot codeها استفاده می شوند. حالا ببینیم hot code چیست؟ hot code یک تابع یا شیءای است که به طور مداوم مورد استفاده قرار می گیرد. V8 یک نسخه باینری کامپایل شده از این تابع یا شیء را، اگر امضای آن ها تغییر نکند، ذخیره می کند. این امر باعث افزایش کارایی فوق العاده ای می شود.

وقتی شما کد متداول خود را در یک تابع قرار داده و قرارداد (پارامتر یا آرگومان ها و نوع آن ها) را تغییر نمی دهید، V8 آن را کامپایل و بهینه سازی خواهد کرد. بنابراین کارایی برنامه افزایش می‌یابد.

از بکارگیری delete اجتناب کنید

ما در ابتدا با C/C++ کار می کردیم و هر زمان که امکان داشت حافظه را آزاد می کردیم.

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

اما زمانی که (JIT (just in time compilers را در موتور جاوااسکریپت شناختیم، متوجه شدیم که با استفاده از کلمه کلیدی delete چه اشتباهات بزرگی را انجام داده‌ایم.

هنگامی که از delete برای حذف ویژگی های شیء استفاده می کنید؛ چیزی شبیه به این است، delete obj.prop، این عملکرد درون کلاس مخفی، آن شیء را تغییر می‌دهد. به این ترتیب، کد کامپایل شده نامعتبر می شود و V8 به طور جداگانه با شیء رفتار می‌کند.

پس اگر واقعا ضروری نیست و می دانید دقیقا چه کاری را انجام می دهید، از delete استفاده نکنید. در عوض می توانید ویژگی را با null تنظیم کنید که کلاس پنهان یا کد کامپایل شده را تغییر نخواهد داد.

Closure و timer

تصور کنید مثال زیر را داشته باشید.

var foo = {
    bar: function () {
        var self = this;
        var timer = setTimeout(function () { 
            console.log('Timeout called'); 
            self.bar();
        }, 100);
    }
};
foo.bar();
foo = null;

یک شیء ساده foo وجود دارد که دارای یک تابع bar می باشد که چیزی جز یک تایمر setTimeout نیست.

پس از فراخوانی bar، foo را به null تنظیم می کنیم. در حالت ایده آل، شیء foo هیچ مرجعی ندارد، و باید حافظه بازیابی شود. اما متأسفانه timer هر زمان که فراخوانی می شود مرجع خود را نگه می دارد. هرگز برای آن شیء بازیابی حافظه انجام نمی شود و باعث نشت حافظه خواهد شد.

بنابراین در سناریوهایی مثل این مورد احتیاط کنید.

ایجاد کلاس برای نوع مشابه از اشیاء

مورد دیگری از کامپایل JIT در اینجا نیز وجود دارد.

هر بار که اشیایی را با امضای مشابه ایجاد می کنید (مثلا با مجموعه ویژگی های یکسان)، سعی کنید یک کلاس یا سازنده برای آن بسازید.

به این ترتیب می توانید یک کلاس مخفی برای همه این نمونه هایی که استفاده می کنید ایجاد کنید و این مسأله دوباره فرصتی به JIT می دهد تا کد شما را بهینه سازی کرده و جاوااسکریپت را سریع‌تر اجرا کند.

بنابراین نمونه ای از این را:

var b = {
    p: 2,
    q: "something else",
    r: true
}
 
var c = {
    p: 10,
    q: "other things",
    r: false
}

اینگونه انجام دهید:

function Some(p, q, r){
    this.p = p;
    this.q = q;
    this.r = r;
}
 
var a = new Some(1, "something", false);
var b = new Some(2, "something else", true);
var c = new Some(10, "other things", false);

forEach در مقایسه با ()for

ممکن است شنیده باشید که استفاده از توابع داخلی همیشه بهتر است. حالا سوال این است، وقتی یک آرایه را تکرار می کنید باید از حلقه for استفاده کنید یا forEach.

خوب، شما باید forEach را ترجیح دهید. این انتخاب همیشه عمل خوبی است. با این حال اگر عنصر تعریف نشده باشد، forEach از شاخص ها اجتناب می کند، که آن را هوشمندانه تر می سازد. مثال زیر می تواند همه چیز را روشن کند.

var arr = new Array(1000);
arr[0] = 20;
arr[434] = 200;
 
for(var i=0; i<arr.length; i++){
    console.log("I am for");
}
 
arr.forEach(function(element){
    console.log("I am forEach");
});

در مثال بالا، متن I am for هزار بار چاپ خواهد شد، اما I am forEach فقط دو بار چاپ خواهد شد.

از for…in اجتناب کنید

به خصوص در مورد object cloning، دیده ایم که مردم از حلقه for...in استفاده می کنند. متأسفانه for...in به شیوه ای طراحی شده است که هرگز نمی تواند کارایی خوبی داشته باشد. پس تا جایی که می توانید از استفاده آن اجتناب کنید. شما می توانید راه های دیگری را برای clone objectها در جاوااسکریپت بررسی کنید.

آرایه های لفظی (Array literal) نسبت به push بهتر هستند

اگر از یک array literal استفاده می کنید، V8 همیشه ساختار بهتری را نسبت به آرایه خالی درک می کند و سپس عناصر را در آن قرار می دهد.

// good
var arr = [1,2,6,2,10,3];
 
// bad
var arr = [];
arr.push(1);
arr.push(2);
arr.push(6);
arr.push(2);
arr.push(10);
arr.push(3);

حالا شما می توانید بگویید که مورد دوم را انتخاب می کنید؟ همه فقط با رویکرد اول کار می کنند. همیشه اینگونه نیست. گاهی اوقات ما ندانسته همانند مورد دوم کد می‌زنیم. مثلا فرض کنید شما هر یک از عناصر آرایه را با قوانین خاصی تغییر می دهید و می خواهید آرایه جدیدی از آن ایجاد کنید.

اغلب توسعه دهندگان ابتدا یک آرایه خالی ایجاد می کنند، سپس روی آرایه قبلی با استفاده از forEach یا هر چیز دیگری عمل تکرار را انجام داده و پس از این عملیات عناصر تغییر یافته را در آرایه جدید قرار می دهند. اما روش بهتر استفاده از متد ()Array.map است.

Web-workerها و  Shared buffer

جاوااسکریپت رشته ای از threadها است. thread برای حلقه رویداد (event-loop) استفاده می شود. بنابراین درخواست جدید شما در node.js در مرورگر مدیریت شده و همه چیز به صورت غیرموازی پردازش می شود.

بنابراین هر وقت که شما کاری را انجام می دهید که زمان محاسباتی بیشتری را می برد، باید آن را به برخی از web-workerها (اجرای کد پشت پرده) محول کنید. در مورد node.js، هیچ worker درونی وجود ندارد، اما می توانید از ماژول npm استفاده کنید یا یک فرآیند جدید برای آن ایجاد کنید.

یک مشکل رایج در کار با workerها می تواند نحوه هماهنگ سازی با آن ها باشد (بدون ارسال پیام پس از اتمام). خوب، SharedArrayBuffer می تواند شیوه سودمندی برای آن باشد.

به نظر می رسد که SharedArrayBuffer بعد از 5 ژانویه 2018 به طور پیش فرض از کار افتاده است؛ اما این مورد (یا نوعی از آن) در مرحله 4 به عنوان پیشنهاد ECMA است.

setImmediate استفاده بیشتر از  نسبت به (setTimeout(fn,0

این نکته فقط برای توسعه دهنده node.js است. بسیاری از توسعه دهندگان از setImmediate یا process.nextTick استفاده نمی کنند و به سمت (setTimeout(fn,0 می روند تا بخشی از برنامه خود را غیرهمزمان ایجاد کنند.

خوب، برخی از تجربیات ما در مورد setImmediate در مقایسه با (setTimeout(fn,0 می گوید، setImmediate می تواند حدود 200 بار  (بله بار نه فقط درصد) سریع تر از (setTimeout(fn,0 باشد.

بنابراین setImmediate را بیشتر از setTimeout استفاده کنید؛ اما در مورد استفاده از process.nextTick احتیاط کنید مگر اینکه نحوه کار آن را درک کرده باشید.

از هاستینگ فایل استاتیک اجتناب کنید

توصیه نمی شود که فایل های استاتیک از سرور node.js خود را به کار گیرید.

چرا؟ زیرا گره ای برای آن ساخته نشده است. گره بهتر می تواند درخواست های tcp/http شما را به کار گیرد، زیرا این کار به طور کامل غیرهمزمان انجام می شود.

اگر شما آن را برای خواندن فایل های استاتیک مشغول نگه داشته اید (این عملیات توسط threadpool libUV انجام می شود)، عملکرد کاهش پیدا خواهد کرد، چرا که libUV دارای تعدادی thread پیش فرض است (به طور پیش فرض 4 تا) که می توانید آن را فقط تا 128 مورد گسترش دهید. اگر وظایف خواندن فایل بیشتر از اندازه معین threadpool وجود داشته باشد، فرآیند شروع به همزمان سازی خواهد کرد.

Promise.all (async await)

Promiseهای بومی ES6 قطعا زندگی ما را آسان تر ساخته اند. اما چیزی که مثل یک موهبت به وجود آمده است async await است. حالا می توانیم کدی را بنویسیم که همزمان به نظر می رسد اما به صورت غیرهمزمان کار خواهد کرد.

اما گاهی اوقات توسعه دهندگان، زیادی کار را راحت می کنند و به صورت ندانسته کد غیرهمزمان غیرضروریی را ایجاد می کنند. یکی از این مثال ها همانند زیر است.

// bad
async function someAsyncFunc() {
    const user = await asyncGetUser();
    const categories = await asyncGetCategories();
    const mapping = await asyncMapUserWithCategory(user, categories);
}
 
// good
async function someAsyncFunc() {
    const [user, categories] = await Promise.all([
        asyncGetUser(),
        asyncGetCategories()
    ]);
    const mapping = await asyncMapUserWithCategory(user, categories);
}

این یک سناریوی رایج برای فراخوانی های چندگانه غیرهمزمان (async) است که توسعه دهندگان یکی پس از دیگری منتظر می مانند. با این حال وقتی تمام آن ها را با استفاده از چیزی مثل ()Promise.all در حالت انتظار برای اجرا قرار می دهید، آن ها می توانند به صورت همزمان تعیین شوند.

شنوندگان رویداد (event listener) را حذف کنید

ما اغلب از شنوندگان رویداد، به ویژه در سمت کلاینت، استفاده می کنیم. و اغلب به دلایل مختلف می توانیم عنصری را که شنونده رویداد وابسته به آن است را حذف کنیم.

اما مسأله قابل توجه این است که شنوندگان رویداد، اگر عنصر وابسته به آن از بین برود، حذف نمی شود. تکرار این مسأله می تواند باعث نشت حافظه شود.

بنابراین باید از زمانی که عناصر از بین می روند مطلع شوید و همچنین شنوندگان رویداد خود را با آن ها حذف کنید.

به جای کتابخانه های کلاینت از بومی (native) استفاده کنید

قبل از اینکه node.js انقلابی به پا کند، جاوااسکریپت فقط در برنامه های سمت کلاینت استفاده می شد.

متأسفانه توسعه دهندگان نمی دانند که مرورگرهای کاربر چه نسخه ای از ECMAScript را مورد استفاده قرار می دهند. به همین دلیل است که کتابخانه های جاوااسکریپت معمولا در سمت کلاینت استفاده می شوند و polyfillها را برای نسخه های مختلف نوشته و کمی سنگین می شوند.

اما وقتی از node.js استفاده می کنید همیشه می دانید که نسخه صحیح و کاملی در حال اجراست. بنابراین به جای استفاده از کتابخانه های سمت کلاینت برای promiseها، کنسول رنگی یا چیزی شبیه آن، همیشه بهتر است که از کد بومی استفاده کنید.

ماژول های باینری

اگر ماژول های بزرگی را برای برنامه خود توسعه می دهید و می خواهید عملکرد را به سطح بعدی ببرید، باید آن ها را به ماژول های باینری کامپایل و تبدیل کنید.

LinkedIn یکی از شرکت هایی است که از node.js به عنوان backend خود استفاده کرده و آن ها از این عمل پیروی می کنند.

از O(n) اجتناب کنید، (O(1 را امتحان کنید

در جاوااسکریپت اشیاء به راحتی نوشته می شوند و شما عینا می توانید هر چیزی (که می تواند به رشته تبدیل شود) را به عنوان یک کلید یا ویژگی پیوست دهید.

در حال حاضر فرض کنید شما لیستی از N دانشجو با شماره فهرست (roll number) خود (که منحصربه فرد است) و تمام علائم (totalMarks) آن را دارید. اگر کسی به شماره فهرست در لیست متنی وارد شود، totalMarkهای آن دانشجو خاص را در صفحه نمایش می دهید.

نمایش رایج این است که شما آرایه ای را ایجاد می کنید و تمام شیءهای دانشجو را در آن قرار می دهید و وقتی کسی وارد شماره فهرست می شود، روی آرایه دانشجویان تکرار انجام شده و آن دانشجوی خاص را پیدا می کنید و علائم آن را نمایش می دهید.

به این ترتیب شما در پیچیدگی O(n) هستید. اما اگر از یک شیء به جای آرایه استفاده کنید و شماره فهرست را به عنوان یک ویژگی و شیء دانشجوی مربوطه را به عنوان مقدار قرار دهید، این موارد برای شما تبدیل به O(1) می شود، و هر وقت کسی شماره فهرست را وارد کرد، شما باید شیء studentsObj[rollNumber] را نمایش دهید.

این مسأله به این معنی نیست که همیشه باید از شیء به جای آرایه استفاده کنید. این امر به موقعیت کاری که می خواهید انجام دهید و الزامات آن بستگی دارد.

نتیجه گیری

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

اما اگر کد شما باعث نشت حافظه یا پردازش پیوسته شود، همه این مراحل dev-ops قادر به ذخیره سرور شما نمی شوند یا حتی کاهش می یابند و یا حتی در هم شکسته و تصادم رخ می دهد.

بنابراین، در حین برنامه نویسی (صرف نظر از زبان استفاده شده)، باید از تمام اقدامات خوب، موارد مناسب و نکاتی که کارایی را بالا می برد آگاه باشید.

ایمان مدائنی

نویسنده 1299 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

تاکنون هیچ کاربری از این پست تشکر نکرده است

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید