بهینه‌سازی برنامه‌های React در عمل برای افزایش سرعت

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 18 مهر 1397
دسته بندی ها : react

React می‌تواند کند باشد. منظورم این است که هر برنامه React با سایز متوسطی می‌تواند کند به نظر بیاید. اما قبل از این که به دنبال جایگزین بگردید، بهتر است بدانید که هر برنامه Angular یا Ember سایز متوسطی هم کند است.

خبر خوب این است که اگر کارایی برای شما مهم است، سریع کردن هر برنامه Reactای بسیار ساده است.

اندازه‌گیری کارایی React

منظور من از «کند» چیست؟ بیایید یک مثال را ببینیم:

من بر روی یک پروژه متن باز به نام admin-on-test کار می‌کنم، که از material-ui و Redux بهره می‌برد تا یک رابط کاربری گرافیکی مدیر برای هر اِی‌پی‌آی RESTای فراهم کند. این برنامه یک صفحه datagrid دارد، که لیستی از رکوردهای جدول را نمایش می‌دهد. وقتی که کاربر ترتیب چینش را تغییر می‌دهد، به صفحه بعدی می‌رود یا نتایج را فیلتر می‌کند، رابط مورد نظر به قدری که انتظار داشتم پاسخگو نیست.

اسکرین‌شات زیر، نشان می‌دهد که بارگذاری مجدد، پنج بار با کاهش سرعت مواجه شد:

برای این که ببینم چه اتفاقی می‌افتد، من یک ?react_perf به URL مورد نظر وصل کردم. این کار، از React 15.4 به بعد، Component Profiling را فعال می‌کند. من منتظر datagrid اولیه می‌مانم تا بارگذاری شود. سپس Chrome Developer Tools را بر روی تب Timeline باز می‌کند، دکمه «Record» را می‌فشارم و بر روی header جدول کلیک می‌کنم تا ترتیب چینش بروزرسانی شود.

بارگذاری‌های مجدد داده را باز کنید، مجددا بر روی دکمه «Record» کلیک کنید و ببینید که Chrome یک نمودار شعله ور زرد به همراه برچسب User Timing نمایش می‌دهد.

اگر تا به حال یک نمودار شعله ور ندیده‌اید، می‌تواند برای شما ترس آور به نظر برسد، اما در واقع استفاده از آن بسیار ساده است. نمودار User Timing، وقت گذشته شده در هر کدام از کامپوننت‌ها را نشان می‌دهد. این نمودار، مدت زمان را در بخش‌های داخلی React مخفی می‌کند، تا بتوانید بر روی بهینه‌سازی برنامه خود متمرکز شوید.

TimeLine اسکرین‌شات‌هایی از ویندوز، در موقعیت‌های مختلف را نشان می‌دهد. این من را قادر می‌سازد تا بر روی لحظه‌ای که بر روی header جدول کلیک کردم، تمرکز کنم:

به نظر می‌رسد که برنامه من دقیقا بعد از کلیک بر روی دکمه چینش، حتی قبل از دریافت داده‌های REST کامپوننت <List> را رندر می‌کند. و این کار بیش از 500 میلی ثانیه زمان می‌برد. برنامه فقط آیکون چینش را در header جدول بروزرسانی می‌کند، و یک پوشش خاکستری بر روی datagrid قرار می‌دهد که نمایانگر این است که داده‌ها در حال دریافت هستند.

به زبان دیگر، برنامه نیم ثانیه زمان می‌برد تا یک پاسخ بصری در قبال یک کلیک نمایش دهد. نیم ثانیه قطعا قابل درک است. متخصصان رابط کاربری می‌گویند که کاربران وقتی که تغییرات یک رابط زیر ۱۰۰ میلی ثانیه باشد، آن را به صورت آنی درک می‌کنند. یک تغییر کندتر از آن، چیزی است که ما «کند» می‌نامیم.

چرا بروزرسانی کردیم؟

در نمودار شعله‌ور بالا، شما چاله‌های کوچک زیادی را می‌بینید. این یک نشانه خوب نیست. معنای آن این است که کامپوننت‌های زیادی رندر شده‌اند. نمودار شعله‌ور نشان می‌دهد که بروزرسانی <Datagrid> بیشترین زمان را می‌برد. چرا برنامه قبل از دریافت داده‌ها، کل datagrid را رندر کرد؟ بیایید به این مسئله وارد شویم.

درک علت رندر کردن، معمولا به اضافه کردن بیانیه console.log() در تابع render() دلالت دارد. برای کامپوننت‌های عملکردی، شما می‌توانید از کامپوننت تک خطی سطح بالای (HOC) زیر استفاده کنید:

// در فایل src/log.js
const log = BaseComponent => props => {
 console.log(`Rendering ${BaseComponent.name}`);
 return <BaseComponent {…props} />;
}
export default log;

// در فایل src/MyComponent.js
import log from ‘./log’;
export default log(MyComponent);

نکته: یکی از ابزار کارایی دیگر React که ارزش اشاره را دارد، why-did-you-update می‌باشد. این پکیج npm، React را وصله می‌کند، تا هر زمان که یک کامپوننت‌ با ویژگی‌های یکسان رندر می‌شود، هشدار کنسول را بیرون بریزد. هشدار: خروجی نهایی عمیق است و بر روی کامپوننت‌های عملکردی کار نمی‌کند.

به عنوان مثال، زمانی که کاربر بر روی یک ستون header کلیک می‌کند، برنامه یک action را بیرون می‌ریزد، که state را تغییر می‌دهد: ترتیب چینش لیست (currentSort) بروزرسانی می‌شود. این تغییر state، رندر کردن صفحه <List> را فعال می‌کند، که در عوض کل کامپوننت <Datagrid> را رندر می‌کند. ما می‌خواهیم این هِدِر Datagrid سریعا رندر شود، تا تغییر آیکون چینش را به عنوان یک پاسخ به کاربر نشان دهد.

چیزی که یک برنامه React را کند می‌کند، معمولا یک کامپوننت تکی کند نیست. (که در نمودار شعله‌ور به شکل یک چاله بزرگ نمایش داده می‌شود) اکثر مواقع چیزی که یک برنامه React را کند می‌سازد، رندر بی معنی چندین کامپوننت است.

ممکن است شنیده باشید که React VirtualDom بسیار سریع است. این مسئله صحیح است، اما در یک برنامه با سایز متوسط، یک redraw کامل می‌تواند صدها کامپوننت را رندر کند. حتی سریع‌ترین موتور الگوی VirtualDom هم نمی‌تواند در کمتر از ۱۶ میلی ثانیه این کار را انجام دهد.

برش کامپوننت‌ها، برای بهینه‌سازی آن‌ها

در اینجا، متد render() کامپوننت <Datagrid> را مشاهده می‌کنید:

// در فایل Datagrid.js
render() {
    const {
        resource,
        children,
        ids,
        data,
        currentSort
    } = this.props;
    return (
        <table>
            <thead>
                <tr>
                    {Children.map(children, (field, index) =>
                        <DatagridHeaderCell
                            key={index}
                            field={field}
                            currentSort={currentSort}
                            updateSort={this.updateSort}
                        />
                    )}
                </tr>
            </thead>
            <tbody>
                {ids.map(id => (
                    <tr key={id}>
                        {Children.map(children, (field, index) =>
                            <DatagridCell
                                record={data[id]}
                                key={`${id}-${index}`}
                                field={field}
                                resource={resource}
                            />
                        )}
                    </tr>
                ))}
            </tbody>
        </table>
    );
}

این به نظر یک پیاده‌سازی بسیار ساده Datagrid می‌باشد، اما بسیار ناکارآمد است. هر فراخوانی <DatagridCell> حداقل دو یا سه کامپوننت را رندر می‌کند. همانطور که می‌توانید در اسکرین‌شات اولیه ببینید، لیست مورد نظر ۷ ستون و ۱۱ ردیف دارد، که یعنی 7 * 11 * 3 = 231 کامپوننت رندر شده. وقتی که فقط currentSort تغییر می‌کند، این کار باعث هدر رفتن زمان می‌شود. با این که React اگر VirtualDom رندر شده تغییر نکرده باشد DOM اصلی را رندر نمی‌کند، پردازش کردن تمام کامپوننت‌ها ۵۰۰ میلی ثانیه زمان می‌برد.

در جهت جلوگیری از یک رندر کردن بیهوده بدنه جدول، من باید آن را «extract» (استخراج) کنم:

// در فایل Datagrid.js
render() {
    const {
        resource,
        children,
        ids,
        data,
        currentSort
    } = this.props;
    return (
        <table>
            <thead>
                <tr>
                    {React.Children.map(children, (field, index) =>
                        <DatagridHeaderCell
                            key={index}
                            field={field}
                            currentSort={currentSort}
                            updateSort={this.updateSort}
                        />
                    )}
                </tr>
            </thead>
            <DatagridBody resource={resource} ids={ids} data={data}>
                {children}
            </DatagridBody>
            </table>
        );
    );
}

من با استخراج منطق بدنه جدول، یک کامپوننت <DatagridBody> جدید ساختم:

// در فایل DatagridBody.js
import React, { Children } from 'react';

const DatagridBody = ({ resource, ids, data, children }) => (
    <tbody>
        {ids.map(id => (
            <tr key={id}>
                {Children.map(children, (field, index) =>
                    <DatagridCell
                        record={data[id]}
                        key={`${id}-${index}`}
                        field={field}
                        resource={resource}
                    />
                )}
            </tr>
        ))}
    </tbody>
);

export default DatagridBody;

استخراج بدنه جدول، هیچ تاثیری روی کارایی ندارد، اما مسیر رو به بهینه‌سازی را نمایان می‌کند. بهینه‌سازی کامپوننت‌های بزرگ و عمومی سخت است؛ اما بهینه‌سازی کامپوننت‌های کوچک و تک مسئولیتی بسیار ساده‌تر است.

shouldComponentUpdate

سند React راه جلوگیری از رندر کردن‌های بیهوده را به صراحت گفته است که: shouldComponentUpdate(). به طور پیشفرض، React همیشه یک کامپوننت را به DOM مجازی رندر می‌کند. به زبانی دیگر، این کار شما به عنوان یک توسعه دهنده است تا بررسی کنید که propهای یک کامپوننت تغییر نکرده‌اند و در آن صورت رندر کردن را به کلی رد کنید.

در مورد کامپونت <DatagridBody> بالا، تا زمانی که propها تغییر کرده‌اند، هیچ رندری از بدنه نباید انجام شود.

پس کامپوننت باید به این صورت کامل شود:

import React, { Children, Component } from 'react';

class DatagridBody extends Component {
    shouldComponentUpdate(nextProps) {
        return (nextProps.ids !== this.props.ids
             || nextProps.data !== this.props.data);
    }

render() {
        const { resource, ids, data, children } = this.props;
        return (
            <tbody>
                {ids.map(id => (
                    <tr key={id}>
                        {Children.map(children, (field, index) =>
                            <DatagridCell
                                record={data[id]}
                                key={`${id}-${index}`}
                                field={field}
                                resource={resource}
                             />
                        )}
                    </tr>
                ))}
            </tbody>
        );
    }
}

export default DatagridBody;

نکته: به عنوان جایگزینی برای پیاده‌سازی دستی shouldComponentUpdate()، من می‌توانستم از PureComponent به جای Component در React استفاده کنم. این کار، تمام propها را با استفاده از علامت «===» مقایسه می‌کرد و فقط در صورتی که هر propای در صفحه تغییر می‌کرد، کامپوننت را رندر می‌کرد. اما من می‌دانم که resource و children نمی‌توانند در آن صورت تغییر کنند، پس نیازی نیست که برابری آن‌ها را بررسی کنم.

با این بهینه‌سازی، رندر کردن کامپوننت <Datagrid> پس از کلیک بر روی یک header جدول، بدنه جدول و کل ۲۳۱ کامپوننت آن را رد می‌کند. این کار، زمان بروزرسانی را از ۵۰۰ میلی ثانیه به ۶۰ میلی ثانیه کاهش می‌دهد. این یک ارتقای بسیار بالاتر از ۴۰۰ میلی ثانیه است.

نکته: نگذارید که عرض نمودار شعله‌ور شما را گول بزند؛ این مورد حتی از نمودار قبلی هم بیشتر زوم شده است و قطعا بهتر است.

بهینه‌سازی shouldComponentUpdate چاله‌های زیادی را از این نمودار حذف کرده، و زمان رندر کلی را کاهش داده است. من می‌توانم از همین حقه برای جلوگیری از رندرهای بیشتر هم استفاده کنم. (برای جلوگیری از رندر نوار کناری، دکمه‌های action، headerهای جدول که تغییر نکرده‌انه و pagination) پس از تقریبا یک ساعت کار، کل صفحه در فقط ۱۰۰ میلی ثانیه پس از کلیک بر روی یک header ستون رندر می‌شود. این به اندازه کافی سریع است؛ حتی اگر همچنان فضای بهینه‌سازی موجود باشد.

اضافه کردن یک متد shouldComponentUpdate شاید سنگین به نظر برسد، اما اگر کارایی برای شما مهم است، همه کامپوننت‌هایی که می‌نویسید بهتر است با یکی از آن‌ها تمام شوند.

این کار را در همه جا انجام ندهید. اجرای shouldComponentUpdate بر روی کامپوننت‌های ساده گاهی اوقات کندتر از فقط رندر کردن کامپوننت است. این کار یک بهینه‌سازی زودگذر خواهد بود. اما همینطور که برنامه‌های شما رشد می‌کنند و می‌توانید ضعف‌های کارایی را در کامپوننت‌های خود بیابید، منطق shouldComponentUpdate را اضافه کنید تا سریع‌تر باشید.

Recompose

من زیاد درباره تغییر قبلی بر روی <DatagridBody> خوشحال نیستم: به علت shouldComponentUpdate، من مجبور بودم که یک کامپوننت ساده و عملکردی را به کامپوننت‌های بر پایه کلاس تغییر شکل دهم. این کار خط کدهای بیشتری اضافه می‌کند، و هر خط کد برای نوشتن، خطایابی و نگهداری هزینه‌ای دارد.

خوشبختانه، شما می‌توانید منطق shouldComponentUpdate را با تشکر از recompose، در کامپوننت سطح بالا (HOC) پیاده‌سازی کنید. این ابزار یک کمربند عملکرد برای React است؛ برای مثال کامپوننت pure():

// در فایل DatagridBody.js
import React, { Children } from 'react';
import pure from 'recompose/pure';

const DatagridBody = ({ resource, ids, data, children }) => (
    <tbody>
        {ids.map(id => (
            <tr key={id}>
                {Children.map(children, (field, index) =>
                    <DatagridCell
                        record={data[id]}
                        key={`${id}-${index}`}
                        field={field}
                        resource={resource}
                    />
                )}
            </tr>
        ))}
    </tbody>
);

export default pure(DatagridBody);

تنها تفاوت بین این کد و پیاده‌سازی اولیه، آخرین خط است: من pure(DatagridBody) را به جای DatagridBody خروجی می‌گیرم. pure مانند PureComponent است، اما boilerplate کلاس اضافی را ندارد.

من حتی می‌توانم دقیق‌تر بود، و با استفاده از shouldUpdate() به جای pure()، فقط propهایی که می‌دانم ممکن است تغییر کنند را هدف قرار دهم:

// در فایل DatagridBody.js
import React, { Children } from 'react';
import shouldUpdate from ‘recompose/shouldUpdate’;

const DatagridBody = ({ resource, ids, data, children }) => (
    ...
);

const checkPropsChange = (props, nextProps) =>
 (nextProps.ids !== props.ids ||
  nextProps.data !== props.data);

export default shouldUpdate(checkPropsChange)(DatagridBody);

تابع checkPropsChange خالص است، و من حتی می‌توانم آن را برای unit test خورجی بگیرم.

کتابخانه recompose کامپوننت‌های سطح بالای پرکاربرد دیگری هم مانند onlyUpdateForKeys() را فراهم می‌کند، که دقیقا نوع بررسی‌ای که من در checkPropsChange انجام دادم را انجام می‌دهد:

// در فایل DatagridBody.js
import React, { Children } from 'react';
import onlyUpdateForKeys from ‘recompose/onlyUpdateForKeys’;

const DatagridBody = ({ resource, ids, data, children }) => (
    ...
);

export default onlyUpdateForKeys([‘ids’, ‘data’])(DatagridBody);

من به شدت recompose را پیشنهاد می‌کنم. فراتر از بهینه‌سازی‌های کارایی، این کتابخانه به شما کمک می‌کند تا منطق دریافت اطلاعات، ترکیبات HOC و دستکاری‌های prop را در یک روش عملکردی استخراج کنید.

Redux

اگر شما از Redux برای مدیریت state برنامه استفاده می‌کنید، پس کامپوننت‌های متصل شده از قبل خالص هستند. نیازی به اضافه کردن یک HOC دیگر نیست.

فقط به یاد داشته باشید که اگر فقط یکی از propها تغییر کند، کامپوننت متصل شده رندر می‌شود. این شامل تمام فرزندان آن هم می‌شود. پس حتی اگر شما از Redux برای کامپوننت‌های صفحه استفاده می‌کنید، بهتر است از pure() و shouldUpdate() برای کامپوننت‌هایی که در شاخه رندر شدن پایین‌تر هستند، استفاده کنید.

همچنین دقت کنید که Redix مقایسه propها را با استفاده از علامت «===» انجام می‌دهد. از آنجایی که Redux به state مربوط به prop یک کامپوننت متصل می‌شود، اگر شما یک شی‌ء را در state جهش دهید، مقایسه propهای Redux آن را از دست خواهد داد. به همین علت باید از immutability (عدم قابلیت جهش) در رندرهای خود استفاده کنید.

برای مثال در admin-on-rest، کلیک بر روی یک header، یک اکشن SET_SORT را فعال می‌کند. reducerای که منتظر آن action است، باید به آبجکت‌های replace در state توجه کند، نه این که آن‌ها را بروزرسانی کند:

// در فایل listReducer.js
export const SORT_ASC = 'ASC';
export const SORT_DESC = 'DESC';

const initialState = {
    sort: 'id',
    order: SORT_DESC,
    page: 1,
    perPage: 25,
    filter: {},
};

export default (previousState = initialState, { type, payload }) => {
    switch (type) {
    case SET_SORT:
        if (payload === previousState.sort) {
            // ترتیب چینش را معکوس کن            return {
                ...previousState,
                order: oppositeOrder(previousState.order),
                page: 1,
            };
        }
        // فیلد چینش را جایگزین کن
        return {
            ...previousState,
            sort: payload,
            order: SORT_ASC,
            page: 1,
        };

    // ...

    default:
        return previousState;
    }
};

با این reducer، وقتی که Redux کد را برای استفاده از سه علامت مساوی بررسی می‌کند، پی می‌برد که آبجکت state متفاوت است و datagrid را رندر می‌کند. اما اگر ما state را جهش داده بودیم، Redux تغییر state را از دست می‌داد و به اشتباه، رندر کردن را رد می‌کرد:

export default (previousState = initialState, { type, payload }) => {
    switch (type) {
    case SET_SORT:
        if (payload === previousState.sort) {
            // هیچ وقت این کار را انجام ندهید
            previousState.order= oppositeOrder(previousState.order);
            return previousState;
        }
        // همچنین این کار را
        previousState.sort = payload;
        previousState.order = SORT_ASC;
        previousState.page = 1;
        return previousState;

    // ...

    default:
        return previousState;
    }
};

برای نوشتن reducerهای غیر قابل جهش، توسعه‌دهندگان دیگر دوست دارند که از immutable.js استفاده کند، که این کتابخانه‌ هم متعلق به Facebook است. از آنجایی که ES6 روند این که به انتخاب خود یک ویژگی کامپوننت را جایگزین کنیم را آسان‌تر می‌کند، من استفاده از این کتابخانه را پیشنهاد می‌کنم. به علاوه، Immutable سنگین است؛ (60 کیلوبایت حجم دارد) پس قبل از اضافه کردن آن به Dependencyهای پروژه خود، حتما فکر کنید.

Reselect

در جهت جلوگیری از رندرهای بیهوده در کامپوننت‌های متصل، شما باید مطمئن شوید که تابع mapStateToProps هر زمان که فراخوانی می‌شود، یک آبجکت جدید را بر نگرداند.

کامپوننت <List> را در admin-on-test به عنوان مثال در نظر بگیرید. این کامپونت لیست رکوردها را با استفاده از این دستور از state برای منبع فعلی می‌گیرد:

// در فایل List.js
import React from 'react';
import { connect } from 'react-redux';

const List = (props) => ...

const mapStateToProps = (state, props) => {
    const resourceState = state.admin[props.resource];
    return {
        ids: resourceState.list.ids,
        data: Object.keys(resourceState.data)
            .filter(id => resourceState.list.ids.includes(id))
            .map(id => resourceState.data[id])
            .reduce((data, record) => {
                data[record.id] = record;
                return data;
            }, {}),
    };
};

export default connect(mapStateToProps)(List);

state شامل آرایه‌ای از تمام رکوردهایی که پیش‌تر دریافت شده‌اند می‌باشد، که بر حسب منبع لیست شده‌اند. برای نمونه، state.admin.posts.data شامل لیست پست‌ها می‌باشد:

{
   23: { id: 23, title: “Hello, World”, /* … */ },
   45: { id: 45, title: “Lorem Ipsum”, /* … */ },
   67: { id: 67, title: “Sic dolor amet”, /* … */ },
}

تابع mapStateToProps این آبجکت state را فیلتر می‌کند، تا فقط رکوردهایی که در واقع بر روی صفحه نمایش داده شده‌اند را برگرداند. چیزی به مانند:

{
    23: { id: 23, title: “Hello, World”, /* … */ },
    67: { id: 67, title: “Sic dolor amet”, /* … */ },
}

مشکل این است که هر زمان mapStateToProps اجرا می‌شود، یک آبجکت جدید را بر می‌گرداند؛ حتی اگر آبجکت اساسی تغییر نکرده باشد. به عنوان یک پیامد، کامپوننت <List> هر زمان که چیزی در state تغییر می‌کند، اجرا می‌شود، در حالیکه id فقط باید در صورتی که داده یا idها تغییر کردند، تغییر کند.

Reselect این مشکل را با حفظ کردن، حل می‌کنم. به جای محاسبه propها مستقیما در mapStateToProps، شما می‌توانید از یک انتخاب کننده موجود در Reselect استفاده کنید، که اگر ورودی تغییر نکند، خروجی مشابه را بر می‌گرداند.

import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'

const List = (props) => ...

const idsSelector = (state, props) =>
    state.admin[props.resource].ids
const dataSelector = (state, props) =>
    state.admin[props.resource].data

const filteredDataSelector = createSelector(
  idsSelector,
  dataSelector,
  (ids, data) => Object.keys(data)
      .filter(id => ids.includes(id))
      .map(id => data[id])
      .reduce((data, record) => {
          data[record.id] = record;
          return data;
      }, {})
)

const mapStateToProps = (state, props) => {
    const resourceState = state.admin[props.resource];
    return {
        ids: idsSelector(state, props),
        data: filteredDataSelector(state, props),
    };
};

export default connect(mapStateToProps)(List);

حال کامپوننت <List> فقط وقتی رندر می‌شود یک زیرمجموعه state تغییر کند.

درباره Recompose هم انتخاب کننده‌های Reselect توابعی خالص هستند که آزمایش و ساخت آن‌ها هم ساده است و این یک راه عالی برای کدنویسی انتخاب کننده‌ها برای کامپوننت‌های متصل Redux است.

مراقب ادبیات آبجکت در JSX باشید

وقتی که کامپوننت‌های شما خالص شوند، شما شروع به تشخیص الگوهای بدی می‌کنید که به رندرهای بیهوده ختم می‌شوند. رایج‌ترین آن‌ها، استفاده از ادبیات‌های آبجکت در JSX است، که من آن را «}} نامعروف» می‌نامم. بگذارید که مثالی به شما نشان دهم:

import React from 'react';
import MyTableComponent from './MyTableComponent';

const Datagrid = (props) => (
    <MyTableComponent style={{ marginTop: 10 }}>
        ...
    </MyTableComponent>
)

ویژگی style کامپوننت <MyTableComponent>، هر زمان که کامپوننت <Datagrid> رندر می‌شود، یک مقدار جدید می‌گیرد. پس حتی اگر <MyTableComponent> خالص باشد، هر زمان که <Datagrid> رندر می‌شود، <MyTableComponent> هم رندر خواهد شد. در واقع، هر زمان که شما یک ادبیات آبجکت را به عنوان یک prop به یک کامپوننت فرزند ارسال می‌کنید، شما خلصت کامپوننت را می‌شکنید. راه حل آن ساده است:

import React from 'react';
import MyTableComponent from './MyTableComponent';

const tableStyle = { marginTop: 10 };
const Datagrid = (props) => (
    <MyTableComponent style={tableStyle}>
        ...
    </MyTableComponent>
)

این کد بسیار پایه به نظر می‌رسد، اما من این اشتباه را به قدری دیده‌ام که شروع به تشخیص «}}» کردم. من به طور معمول آن را با constantها جایگزین می‌کنم.

یک مورد مشکوک رایج دیگر برای ربودن کامپوننت‌های خالص، React.cloneElement() است. اگر شما یک prop را بر حسب مقدار به عنوان دومین پارامتر منتقل کنید، عنصر کپی شده بر روی هر رندر propهای جدیدی را دریافت خواهد کرد.

// بد
const MyComponent = (props) =>
    <div>{React.cloneElement(Foo, { bar: 1 })}</div>;

// خوب
const additionalProps = { bar: 1 };
const MyComponent = (props) =>
    <div>{React.cloneElement(Foo, additionalProps)}</div>;

این مسئله یکی دو بار با material-ui من را اذیت کرده است. برای مثال این کد:

import { CardActions } from 'material-ui/Card';
import { CreateButton, RefreshButton } from 'admin-on-rest';

const Toolbar = ({ basePath, refresh }) => (
    <CardActions>
        <CreateButton basePath={basePath} />
        <RefreshButton refresh={refresh} />
    </CardActions>
);

export default Toolbar;

گرچه <CreateButton> خالص است، اما هر زمان که <Toolbar> رندر شود، <CreateButton> هم رندر می‌شود. علت آن این است که <CardActions> در material-ui یک استایل خاص به اولین فرزند خود اضافه می‌کند تا با حاشیه‌ها تطبیق یابد، و این کار را با استفاده از یک ادبیات آبجکت انجام می‌دهد. پس هر بار که <CreateButton> یک prop استایل متفاوت را دریافت کرد، من این مسئله را با استفاده از onlyUpdateForKeys() حل کردم.

// در فایل Toolbar.js
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';

const Toolbar = ({ basePath, refresh }) => (
    ...
);

export default onlyUpdateForKeys(['basePath', 'refresh'])(Toolbar);

نتیجه گیری

چندین کار دیگر هستند که بهتر است برای تسریع برنامه React خود انجام دهید، (مانند استفاده از کلیدها، lazy load کردن routeهای سنگین، پکیج react-addons-perf، استفاده از ServiceWorkerها برای cache کردن state برنامه و...) اما پیاده‌سازی shouldComponentUpdate قطعا اولین و امیدوارکننده‌ترین قدم است.

React به طور پیشفرض سریع نیست، اما تمام ابزار مورد نیاز برای تسریع برنامه، بدون توجه به اندازه آن را دارد.

این کار ممکن است بر خلاف معمول به نظر برسد؛ به خصوص با توجه به این که فریم‌وورک‌های زیادی که جایگزین React هستند، ادعا می‌کنند که چندین برابر سریع‌تر هستند. اما React تجربه کاربران را مهم‌تر از کارایی در نظر می‌گیرد. علت این که توسعه برنامه‌های بزرگ با استفاده از React بسیار لذت‌بخش است نیز همین می‌باشد.

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

منبع

مقالات پیشنهادی

6 راه ساده برای افزایش سرعت برنامه React Native

قبلا درباره بهبود کارایی برنامه با هک کردن Event loop صحبت کرده ام. هدف رسیدن به 60 فریم بر ثانیه است. تمام این ها در مورد React یا React Native هم صد...

10 راه بهنیه سازی و افزایش سرعت وبسایت وردپرسی - بخش اول

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

افزایش سرعت وبسایت با استفاده از HTTP/2

HTTP/2 راهی جدید برای سریع‌تر کردن میزان زمان بارگذاری وبسایت با حذف کردن برخی از ویژگی‌های ناکارآمد در HTTP است. پیاده‌سازی HTTP/2 کار سختی نیست و نی...

راهنمای سریع برای کمک به شما در درک و ساخت برنامه‌های ReactJS

اولین بخش، نحوه ساخت یک برنامه React ساده با استفاده از create-react-app را نشان داده و ساختار پروژه را شرح می‌دهد.بخش دوم یک قطعه کد که از قبل بر روی...