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

شنبه 24 آذر 1397

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

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

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

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

منظور من از "کند" چیست؟ بیایید به یک مثال بپردازیم:

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

تصویر  زیر رفرش آهسته این عملکرد را نشان می‌دهد:

برای دیدن آنچه اتفاق می‌افتد ما react_perf؟ را به URL اضافه کرده‌ایم.  این امر پس از React 15.4، Component Profiling را فعال می‌کند. ما منتظریم datagrid اولیه لود شود. سپس Chrome Developer Tools را روی تب Timeline باز می‌کنیم. دکمه " Recor" را می‌زنیم و روی هدر جدول کلیک می‌کنیم تا مرتب‌سازی آپدیت شود.

هنگامی که داده‌ها رفرش می‌شوند، ما دکمه " Record" را دوباره فشار می‌دهیم تا ثبت آن متوقف شود، و کروم یک نمودار خطی زرد رنگ را با برچسب " User Timing" نشان می‌دهد.

اگر شما هرگز این نمودار زرد رنگ را ندیده باشید، ممکن است برایتان ترسناک باشد، اما استفاده از آن واقعا آسان است. نمودار " User Timing" زمان سپری شده برای هر کامپوننت شما را نشان می‌دهد. این نمودار زمان صرف شده در بخش‌های داخلی React را پنهان می‌سازد (در هر صورت شما نمی‌توانید این زمان را بهینه‌سازی کنید)، به طوری که به شما اجازه می‌دهد روی بهینه‌سازی برنامه خود تمرکز کنید.

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

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

در غیر این صورت برای قرار دادن آن، برنامه نیم ثانیه طول می‌کشد تا بازخورد بصری را برای یک کلیک ارائه دهد. نیم ثانیه قطعا قابل درک است؛ متخصصان  UI می‌گویند که وقتی زمان این تغییرات کمتر از 100ms باشد کاربران تغییر را به صورت آنی درک می‌کنند. تغییر خیلی کند صورت می‌گیرد.

چرا آپدیت کنیم؟

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

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

// in src/log.js
const log = BaseComponent => props => {
 console.log(`Rendering ${BaseComponent.name}`);
 return <BaseComponent {…props} />;
}
export default log;

// in src/MyComponent.js
import log from ‘./log’;
export default log(MyComponent);

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

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

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

ممکن است شنیده باشید که React VirtualDom خیلی سریع است. این درست است، اما در یک برنامه با حجم متوسط، یک redraw کامل به راحتی می‌تواند صدها کامپوننت را رندر کند. حتی سریع‌ترین موتور الگوی VirtualDom هم نمی‌تواند آن را در کمتر از 16ms ایجاد کند.

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

در اینجا ما کامپوننت Datagrid>> متد render() را داریم:

// in 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 سطر دارد، که این یعنی 231 = 7*11*3 کامپوننت رندر شده. وقتی فقط currentSort تغییر می‌کند، در واقع این کار یک اتلاف وقت است.

حتی با اینکه React، اگر VirtualDom رندر شده تغییر نکرده باشد، DOM واقعی را آپدیت نمی‌کند، باز هم 500ms برای پردازش همه کامپوننت‌ها زمان می‌برد.

برای اجتناب از رندرهای غیرضروری بدنه جدول، باید ابتدا آن را استخراج کنیم:

// in 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> را توسط استخراج منطق بدنه جدول ایجاد کرده‌ایم:

// in 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 استفاده کنیم. این عمل همه propها را با استفاده از علامت تساوی (===) مقایسه کرده و فقط اگر هر prop تغییر کند، کامپوننت را رندر می‌کند. اما می‌دانیم که در این حالت resource  وchildren  نمی‌توانند تغییر کنند، بنابراین نیازی نیست تا تساوی آن‌ها را بررسی کنیم.

با این بهینه‌سازی، رندر کامپوننتDatagrid>> بعد از کلیک روی هدر جدول، بدنه جدول و 231 کامپوننت آن را به طور کامل رد می‌کند. این کار زمان آپدیت را از 500ms به 60ms کاهش می‌دهد که یک پیشرفت کارایی بالاتر از 400ms است!

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

بهینه‌سازی shouldComponentUpdate تعداد زیادی حفره در نمودار زرد رنگ را از بین می‌برد و زمان کلی رندر کردن را کاهش می‌دهد. ما می‌توانیم از ترفندهای مشابهی برای جلوگیری از رندرهای بیشتر استفاده کنیم. (مثلا برای جلوگیری از رندر کردن sidebar، دکمه‌های اقدامات، هدرهای جدول که تغییر نمی‌کرده‌اند، صفحه ‌بندی). پس از حدود یک ساعت کار، تمام صفحه پس از کلیک روی ستون هدر فقط در 100ms رندر می‌شود. این روند به اندازه کافی سریع است؛ حتی اگر هنوز هم جا برای بهینه‌سازی باشد.

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

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

Recompose

من به خاطر shouldComponentUpdate چندان با تغییرات قبلی بر روی < DatagridBody > موافق نیستم. ما مجبور شدیم یک کامپوننت ساده و عملکردی را به کامپوننتی بر پایه کلاس تبدیل کنیم. این کار خطوط کد بیشتری را اضافه کرده، و هر خط کد برای نوشتن، خطایابی، و نگهداری هزینه‌ای دارد.
خوشبختانه شما می‌توانید به لطف recompose منطق shouldComponentUpdate را در یک کامپوننت عملکردی (HOC) پیاده سازی کنید. این یک کار سودمند کاربردی برای React است. مثلا pure() HOC:

// in 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()ی recompose به جای pure()، فقط propهایی که می‌دانیم ممکن است تغییر کنند را هدف قرار دهیم.

// in 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 خالص (pure) است، و من حتی می‌توانم آن را برای آزمایش واحد (unit test) اکسپورت کنم.

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

// in 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 برای مدیریت وضعیت برنامه استفاده می‌کنید (که ما توصیه می‌کنیم)، پس کامپوننت‌های مرتبط در حال حاضر خالص هستند. نیازی به افزودن HOC دیگری نیست.

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

همچنین مراقب باشید که Redux، مقایسه propها را با علامت تساوی انجام می‌دهد. از آنجا که Redux به state مربوط به propهای کامپوننت متصل می‌شود، اگر شما شیءای را در state تغییر دهید، مقایسه propهای Redux آن را از دست خواهند داد. به همین دلیل باید از ثبات در reducerهای خود استفاده کنید.

برای مثال، در admin-on-rest، کلیک بر روی هدر جدول یک اکشن SET_SORT را ارسال می‌کند. Reducerای که منتظر آن اکشن است باید توجه خود را به جابجایی اشیاء در state بدهد، نه اینکه آن‌ها را به‌روزرسانی کند.

// in 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) {
            // inverse sort order
            return {
                ...previousState,
                order: oppositeOrder(previousState.order),
                page: 1,
            };
        }
        // replace sort field
        return {
            ...previousState,
            sort: payload,
            order: SORT_ASC,
            page: 1,
        };

    // ...

    default:
        return previousState;
    }
};

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

// don't do this at home
export default (previousState = initialState, { type, payload }) => {
    switch (type) {
    case SET_SORT:
        if (payload === previousState.sort) {
            // never do this
            previousState.order= oppositeOrder(previousState.order);
            return previousState;
        }
        // never do that either
        previousState.sort = payload;
        previousState.order = SORT_ASC;
        previousState.page = 1;
        return previousState;

    // ...

    default:
        return previousState;
    }
};

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

 Reselect

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

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

// in 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 تغییر کند رندر می‌شود.

همان‌طور که اشاره شد انتخابگرهای reselect توابعی هستند که ساخت و تست آن‌ها آسان است. این روشی عالی برای کدنویسی انتخاب‌گرهای شما برای کامپوننت‌های متصل‌ Redux می‌باشد.

مراقب Object Literalها در JSX باشید

هنگامی که کامپوننت‌های شما خالص می‌شوند، شما شروع به یافتن الگوهای بدی می‌کنید که رندرهای غیرضروری را هدایت می‌کند. رایج‌ترین آن‌ها استفاده از object literalها در JSX است که ما دوست داریم آن را " {{  افتضاح" بنامیم. بگذارید یک مثال بزنیم:

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

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

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

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

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

این کد بسیار پایه‌ای به نظر می‌رسد، اما ما این اشتباه را چندین بار دیده‌ایم که ما مفهومی را برای تشخیص {{ در JSX توسعه داده‌ایم. ما آن را با constantها جایگزین کردیم.

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

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

// good
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 استایل خاصی را به اولین فرزند خود اضافه می‌کند تا با حاشیه‌ها تطبیق یابد و این کار را با object literal انجام می‌دهد. بنابراین <CreateButton> استایل متفاوت prop را دریافت می‌کند. ما این مساله را با استفاده از onlyUpdateForKeys() HOC حل کردیم.

// in Toolbar.js
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';

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

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

نتیجه‌گیری

موارد بسیار دیگری وجود دارند که شما باید انجام دهید تا برنامه React خود را سریع نگه دارید (با استفاده از کلیدها، لیزی لودینگ برای مسیرهای سنگین، بسته‌های react-addons-perf، استفاده از ServiceWorkers برای کش کردن state برنامه و غیره) اما قطعا پیاده‌سازی shouldComponentUpdate اولین و پر سودترین گام است.

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

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

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

برای آشنایی بیشتر با مفهوم React به آموزش React در تاپ لرن مراجعه نمایید.

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

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

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

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

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