برنامه‌نویسی

تکنیک‌هایی برای افزایش سرعت برنامه‌های Node.js


۲۰ اردیبهشت ۱۴۰۰
تکنیک‌هایی برای افزایش سرعت برنامه‌های node.js

به‌احتمال زیاد زبان JavaScript از اولین انتخاب‌های برنامه‌نویسان برای توسعه‌ی برنامه‌های وب خواهد بود و آمارهای StackOverflow می‌تواند این موضوع را تایید کند. علاوه‌بر محبوبیت بسیار بالای این زبان باید به سادگی ترکیب این زبان با تکنولوژی‌های دیگر اشاره داشته باشیم زیرا این موضوع شما را قادر می‌سازد تا برنامه‌های مختلف دیگری را توسعه دهید یا در پروژه‌های مختلفی همکاری داشته باشید.

حال این روز‌ها با حضور Node.js برای برنامه‌نویسی بک‌اند می‌توان گفت که زبان JavaScript توانسته به یک تکنولوژی کلیدی در برنامه‌های وب تبدیل شود. همچنین با افزایش رقابت در بازار، اکثر مشاغل در جستجوی ابزار یا فناوری‌ای هستند که آن‌ها را قادر سازد با یک راه حل به چندین نیاز پاسخ دهند. بنابراین کسب و کارها برای تامین نیازهایشان می‌توانند از Node.js برای توسعه‌ی برنامه‌ها در بک‌اند و از تکنولوژی‌هایی مانند React، Vue یا Angular برای توسعه‌ی فرانت‌اند برنامه‌ها استفاده کنند.

البته توسعه‌ی برنامه‌ها با Node.js می‌تواند چالش‌های جدیدی برای شما ایجاد کند که قبل از رسیدگی به آن چالش‌ها باید با مفاهیم ابتدایی و اولیه این تکنولوژی آشنا شوید.

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

مقاله‌ی مرتبط: Deno چیست و آیا جایگزین NodeJS خواهد شد؟

معماری event-driven و non-blocking I/O از مشخصه‌های برجسته‌ی Node.js هستند که مقیاس‌پذیری برنامه‌ها را تضمین می‌کنند. علاوه‌براین یکی دیگر از ویژگی‌های برجسته‌ی Node.js وجود یک کتابخانه‌ی داخلی است که به شما کمک می‌کند بدون استفاده از ابزارهایی مانند Apache HTTP یا IIS، یک وب سرور داشته باشید.

معماری event-driven در Node.js یک فناوری ایده‌آل برای برنامه‌های real-time مانند برنامه‌های چت یا استریم است و از آنجا که client-side و server-side برنامه با JavaScript توسعه داده می‌شود، روند همگام‌سازی داده‌ها بهتر و سریع‌تر انجام خواهد شد.

مقاله‌ی مرتبط: چرا از NodeJS استفاده کنیم؟

استراتژی‌های افزایش سرعت برنامه‌های Node.js

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

بنابراین افزایش سرعت لود صفحه‌های وب در موفقیت شما بسیار تاثیرگذار خواهد بود و باید در ۱۰ ثانیه اول نظر کاربر را جلب کنید بنابراین انتخاب Node.js بسیار هوشمندانه است زیرا این تکنولوژی با معماری event-driven و non-blocking یا همان asynchronous توانسته در توسعه‌ی برنامه‌های سریع و مقیاس‌پذیر نقش‌آفرینی کند.

مقاله‌ی مرتبط: Synchronous و Asynchronous چیست؟

استفاده محدود از فانکشن‌های Synchronous

از آنجا که Node.js با معماری single thread طراحی شده است بنابراین در برنامه‌هایی که با این تکنولوژی توسعه داده می‌شوند شاهد کدهای Asynchronous بسیار زیادی هستیم اما با استفاده‌ از کامپوننت‌های Synchronous، جریان عملیاتی non-blocking برنامه مسدود خواهد شد و درنتیجه شاهد عملکرد کندتر برنامه خواهیم بود.

var fs = require('fs');

// Asynchronous
fs.readFile('file.txt', (err, data) => {
    var content = data.toString();
});

// Synchronous
var contents = fs.readFileSync('file.txt').toString();

افزایش بهره‌وری برنامه با استفاده از Caching

Caching یک روش ساده و رایج است که به شما امکان می‌دهد داده‌ها را به‌صورت موقت در جایی ذخیره کرده و آن‌ها را در مواقع نیاز به کاربر نمایش دهید. به این شکل ارسال responseها سریع‌تر انجام خواهد شد و حتی هزینه‌های برنامه کاهش پیدا می‌کند.

البته در صورتی که کاربران زیادی نداشته باشید، Caching تاثیر زیادی در بهبود سرعت برنامه‌ی شما نخواهد داشت اما عملکرد برنامه‌ها زمانی با مشکل روبرو می‌شود که میزان ترافیک برنامه‌ی شما افزایش پیدا کند و در همین حال شما باید تعادل بار را حفظ کنید. بنابراین در حالی که با این مشکل روبرو باشید، Caching راه حلی بسیار مناسب برای دستیابی به عملکرد سریع‌تر برنامه خواهد بود.

البته پیاده‌سازی این سیستم می‌تواند کمی پیچیده باشد بنابراین شما به ابزارهایی نیاز خواهید داشت که در این مسیر به شما کمک کنند.

  • Memcached: داده‌ها به کمک Memcached stores می‌توانند در nodeهای مختلف ذخیره شوند و در همین حال با بهره بردن از hashing Schema امکان استفاده از عملکرد hash table را برای شما فراهم خواهد کرد.
  • Node-Cache: با متدهای set، get و delete می‌توانیم عملکرد این پکیج نرم‌افزاری را شبیه به Memcached بدانیم. همچنین شما می‌توانید برای حذف داده‌های cache شده یک زمان معین قرار دهید تا پس از مدت زمان معلوم، پاک شوند.
  • Nginx: این وب سرور محبوب می‌تواند به شما در cache کردن فایل‌های استاتیک برنامه کمک کند و این کار باعث کاهش بار سرور خواهد شد.
  • Redis Cache: قبل‌تر در مقاله‌‌ای با عنوان Redis چیست؟ به معرفی این دیتابیس و مقایسه‌ی آن با Memcached پرداخته بودیم اما می‌توان Redis را یک نسخه‌ی کامل‌تر از Memcached دانست که به کمک آن می‌توانید زمان لود صفحه‌ها را کاهش دهید و درنهایت برنامه‌ی سریع‌تری داشته باشید.

برای درک بهتر عملکرد Caching در افزایش سرعت برنامه‌های وب به سراغ یک مثال ساده می‌رویم که در آن هیچ داده‌ای cache نمی‌شود:

'use strict'

// Define all dependencies needed
const express = require('express');
const responseTime = require('response-time');
const axios = require('axios');

// load Express Framework
var app = express();

// Create a middleware that adds a X-Reponse-Time header to responses
app.use(responseTime());

const getBook = (req, res) => {
    let isbn = req.query.isbn;
    let url = `https://www.googleapis.com/books/v1/volumes?q=${isbn}`;
    axios.get(url)
        .then((response) => {
            let book = response.data.items;
            res.send(book);
        })
        .catch(err => {
            res.send('The book you are looking for is not found!');
        })
};

app.get('/book', getBook);

app.listen(3000, function () {
    console.log('server is running');
})

پس از اجرای کدهای فوق می‌توانید به آدرس localhost:3000/book?isbn=12 رفته و مقدار X-Reponse-Time را در Response Header مشاهده کنید. در ادامه می‌خواهیم سیستم Caching را به همین کدهای ساده اضافه کنیم:

'use strict'

// Define all dependencies needed
const express = require('express');
const responseTime = require('response-time');
const axios = require('axios');
const redis = require('redis');
const client = redis.createClient();


// load Express Framework
var app = express();

// Create a middleware that adds a X-Reponse-Time header to responses
app.use(responseTime());

const getBook = (req, res) => {
    let isbn = req.query.isbn;
    let url = `https://www.googleapis.com/books/v1/volumes?q=${isbn}`;
    axios.get(url)
        .then((response) => {
            let book = response.data.items;

            client.setex(isbn, 3600, JSON.stringify(book));

            res.send(book);
        })
        .catch(err => {

            res.send('The book you are looking for is not found!');

        })
};

const getCache = (req, res) => {
    let isbn = req.query.isbn;

    client.get(isbn, (err, result) => {
        if (result) {
            res.send(result);
        } else {
            getBook(req, res);
        }

    })

}

app.get('/book', getCache);

app.listen(3000, function () {
    console.log('server is running');
})

البته توجه داشته باشید که برای استفاده از Redis، باید redis-server شما در پورت پیش‌فرض 6379 اجرا شده باشد. حال با مقایسه‌ی مقدار X-Reponse-Time این دو برنامه یکسان که فقط در یکی از آن‌ها از سیستم Caching استفاده شده متوجه شوید که Caching تاثیر بسیار زیادی در بهینه‌سازی عملکرد برنامه‌ی شما خواهد داشت.

بهینه‌سازی کوئری‌های درخواست اطلاعات از دیتابیس

فرض کنید که در حال توسعه‌ی یک وبلاگ هستید و می‌خواهید آخرین پست‌ها را در صفحه‌ی اصلی نمایش دهید. به احتمال زیاد اولین راه حل شما برای fetch کردن داده‌ها با استفاده از Mongosse به شکل زیر خواهد بود:

Post.find().limit(10).exec(function (err, posts) {
    // Send posts to client
});

اما بزرگ‌ترین چالشی که کدهای بالا برای شما ایجاد می‌کنند این است که شما با استفاده از متد find() تمام فیلد‌های ده Post آخر در دیتابیس را fetch خواهید کرد اما با بهینه‌سازی کوئری می‌توانیم برنامه‌ی سریع‌تری داشته باشیم اما چگونه؟ بیایید کمی موضوع را عمیق‌تر بررسی کنیم. یکی از فیلد‌هایی که ما در صفحه‌ی اول وبسایت به آن نیازی نداریم، کامنت‌ها هستند که تعداد آن‌ها می‌تواند قابل توجه باشد بنابراین با استثنا قرار دادن کامنت‌ها می‌توانیم کوئری بهینه‌تری برای درخواست اطلاعات به دیتابیس ارسال کنیم:

Post.find().limit(10).exclude('comments').exec(function (err, posts) {
    // Send posts to client
});

زیر نظر داشتن عملکرد برنامه با Logging

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

البته یک رویکرد در میان برنامه‌نویسان وجود دارد که همیشه از console.log() برای Logging برنامه استفاده می‌کنند اما این گزارش‌ها به‌عنوان خروجی استاندارد شناسایی می‌شوند بنابراین شما برای log کردن خطاها باید از console.error() استفاده کنید که یک روش استاندارد است.

همچنین برای Logging پیشرفته‌تر نیز ابزارهایی مانند Winston، Morgan و Bunyan وجود دارند که می‌توانند به شما در این فرایند کمک کنند و ما در ادامه می‌خواهیم نحوه‌ی نصب و پیکربندی Winston را به شما نشان دهیم.

برای نصب پکیج Winston در یک پروژه باید دستور‌ زیر را اجرا کنید:

npm i winston --save

اما پیکربندی این پکیج نرم‌افزاری کمی پیچیده‌تر است:

const winston = require('winston');

let logger = winston.createLogger({
    transports: [
        new winston.transports.File({
            level: 'verbose',
            timestamp: new Date(),
            filename: 'verbose-log.log',
            json: false,
        }),
        new winston.transports.File({
            level: 'error',
            timestamp: new Date(),
            filename: 'error-log.log',
            json: false,
        })
    ]
});

logger.error('error msg');

اجرای موازی فانکشن‌ها

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

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

function runInParallel() {
    async.parallel([
        getUserProfile,
        getRecentActivity,
        getSubscriptions,
        getNotifications
    ], function (err, result) {
        // this callbacl runs when all the functions compeleted
    });
}

منبع: https://javascript.plainenglish.io/best-tricks-to-make-your-node-js-web-app-faster-9e19dd28aa5b