پرش به محتوا

چگونه عملکرد اپلیکیشن‌های OWL را در Odoo 19 بهینه کنیم؟



1405/02/24 توسط
چگونه عملکرد اپلیکیشن‌های OWL را در Odoo 19 بهینه کنیم؟
رهام ایزدی


اگر تا به حال چیزی در رابط کاربری اودوو ساخته باشید، احتمالاً می‌دانید که OWL یا Odoo Web Library از همان ابتدا به‌عنوان یک کتابخانه واکنشی و مبتنی بر کامپوننت شناخته می‌شود. OWL به‌صورت پیش‌فرض عملکرد قابل قبولی دارد. با این حال، تنظیمات پیش‌فرض خوب فقط تا زمانی کافی هستند که اپلیکیشن شما پیچیده‌تر نشده باشد؛ مثلاً داشبوردهای زنده، صفحات POS یا گزارش‌هایی که هزاران خط داده دارند.

اگر در توسعه چنین بخش‌هایی بی‌دقت عمل کنید، خیلی زود با مشکلات عملکردی روبه‌رو می‌شوید؛ مشکلی که در دنیای اپلیکیشن‌های سازمانی، اصلاً موضوعی لوکس یا کم‌اهمیت نیست. این مقاله روی استراتژی‌هایی تمرکز دارد که در عمل بیشترین کمک را برای بهینه‌سازی عملکرد OWL داشته‌اند.


درخواست مشاوره و دمو 


چه چیزهایی واقعاً روی عملکرد OWL تأثیر می‌گذارند؟


قبل از اینکه سراغ راهکارها برویم، مهم است بدانیم مشکل معمولاً از کجا شروع می‌شود. عملکرد OWL عمدتاً به چهار بخش وابسته است:

  • ساختار کامپوننت‌های شما چگونه طراحی شده است؟
  • state یا وضعیت را چگونه مدیریت می‌کنید؟
  • داده‌ها را چگونه و چه زمانی از بک‌اند دریافت می‌کنید؟
  • رابط کاربری چند بار نیاز به به‌روزرسانی دارد؟

اگر هرکدام از این موارد اشتباه مدیریت شود، ممکن است با رابط کاربری‌ای روبه‌رو شوید که دائماً دوباره رندر می‌شود، بک‌اند را با درخواست‌های غیرضروری تحت فشار قرار می‌دهد یا هنگام استفاده، کند و سنگین به نظر می‌رسد.

بیایید هرکدام را بررسی کنیم.

اپلیکیشن‌های OWL

مشکلات رایج عملکردی که احتمالاً با آن‌ها روبه‌رو می‌شوید


بیشتر مشکلات عملکردی OWL به چند اشتباه تکراری برمی‌گردند. اگر این موارد را زود تشخیص دهید، زمان زیادی در دیباگ صرفه‌جویی می‌شود:

  • کامپوننت‌ها خیلی بیشتر از حد لازم دوباره رندر می‌شوند.
  • داده‌های بیش از حد داخل state واکنشی قرار داده می‌شود.
  • تعداد زیادی RPC یا API call ارسال می‌شود؛ گاهی حتی یک درخواست چندین بار تکرار می‌شود.
  • محاسبات سنگین مستقیماً داخل template قرار می‌گیرند.
  • یک لیست بسیار بزرگ به‌صورت یکجا رندر می‌شود.

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


مدیریت State

State در OWL ماهیتی واکنشی دارد. هر تغییری در آن باعث رندر دوباره کامپوننت می‌شود. این یک سمت ماجراست؛ اما از طرف دیگر، همین موضوع می‌تواند به نقطه‌ضعف اپلیکیشن تبدیل شود. اگر داده‌ای داخل state object وجود داشته باشد که واقعاً در رابط کاربری لازم نیست، رندرهای غیرضروری اتفاق می‌افتد.

قاعده ساده من برای state این است: فقط چیزهایی را داخل state ذخیره کنید که واقعاً باید باعث به‌روزرسانی رابط کاربری شوند.


بهترین روش‌ها

  • state را سبک نگه دارید و فقط چیزی را ذخیره کنید که template واقعاً به آن نیاز دارد.
  • از قرار دادن objectها یا arrayهای بزرگ و تو در تو داخل state واکنشی خودداری کنید.
  • فقط همان فیلدی را که تغییر کرده به‌روزرسانی کنید، نه کل object را.


مثال

this.state = useState({
count: 0,
});

این کار سطح واکنشی را بسیار کوچک نگه می‌دارد. به‌روزرسانی count فقط یک re-render هدفمند ایجاد می‌کند و چیز بیشتری را درگیر نمی‌کند.


کاهش Re-renderهای غیرضروری


رندر شدن بیش از حد احتمالاً رایج‌ترین مشکل عملکردی OWL است که در پروژه‌های واقعی دیده می‌شود. بخش سخت ماجرا این است که این مشکل همیشه قابل مشاهده نیست؛ ممکن است با نگاه کردن به صفحه متوجه آن نشوید، اما مرورگر قطعاً آن را حس می‌کند.


چگونه از آن جلوگیری کنیم؟


  • تا زمانی که واقعاً چیزی تغییر نکرده، state را به‌روزرسانی نکنید.
  • کامپوننت‌های بزرگ را به کامپوننت‌های کوچک‌تر و متمرکز تقسیم کنید؛ یک child component فقط زمانی دوباره رندر می‌شود که props خودش تغییر کند.
  • هرگز داخل loop باعث به‌روزرسانی state نشوید؛ تغییرات را batch کنید یا منطق را بازطراحی کنید.


مثال

this.state.count++; // only this field updates -- minimal re-render

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


دریافت داده؛ زمان‌بندی همه چیز است

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


برای بارگذاری اولیه داده‌ها از willStart() استفاده کنید

متد willStart() قبل از اولین رندر کامپوننت اجرا می‌شود؛ بنابراین بهترین محل برای دریافت داده‌هایی است که template شما به آن‌ها نیاز دارد. تا زمانی که این متد کامل نشود، کامپوننت رندر نمی‌شود؛ در نتیجه کاربر یک صفحه خالی یا ناقص نمی‌بیند.

async willStart() {
this.data = await this.rpc("/api/data");
}

برای داده‌هایی که باید هنگام تغییر props به‌روزرسانی شوند، از onWillUpdateProps() استفاده کنید؛ اما برای بارگذاری اولیه، willStart() معمولاً انتخاب اصلی است.


کاهش RPC Callها

هر درخواست RPC یک رفت‌وبرگشت به سرور است. حتی اگر اتصال شبکه شما سریع باشد، این درخواست‌ها می‌توانند روی هم جمع شوند. در چارچوب اودوو، اگر یک بخش هنگام بارگذاری پنج درخواست جداگانه searchRead ارسال کند، پنج برابر بیشتر از چیزی که لازم است کار انجام می‌دهد.


بهترین روش‌ها

  • هرگز یک API call مشابه را دوبار ارسال نکنید؛ نتیجه را cache کنید.
  • برای درخواست‌های مستقل، به جای اجرای پشت‌سرهم، از Promise.all() استفاده کنید تا به‌صورت موازی اجرا شوند.
  • اگر چند فیلد می‌توانند از یک model دریافت شوند، آن‌ها را در یک درخواست واحد بگیرید.


مثال: اجرای درخواست‌ها به‌صورت موازی

// Slow -- calls run one after the other:
const customers = await this.orm.searchRead('res.partner', [], ['name']);
const invoices = await this.orm.searchRead('account.move', [], ['name', 'partner_id']);

// Fast -- both calls fire at the same time:
const [customers, invoices] = await Promise.all([
this.orm.searchRead('res.partner', [], ['name']),
this.orm.searchRead('account.move', [], ['name', 'partner_id'])
]);


مثال: Cache کردن پاسخ

async willStart() {
if (!this.cachedData) {
this.cachedData = await this.orm.searchRead(
'product.product', [], ['name', 'list_price']
);
}
this.state.products = this.cachedData;
}

این الگو مخصوصاً برای داده‌های مرجع بسیار مفید است؛ مانند ارزها، واحدهای اندازه‌گیری و دسته‌بندی‌های محصول که معمولاً در طول یک session به‌ندرت تغییر می‌کنند.

اپلیکیشن‌های OWL را در Odoo 19

مدیریت Datasetهای بزرگ بدون فریز شدن UI


اگر هزار آیتم را مستقیماً داخل یک t-foreach قرار دهید، تقریباً مطمئن باشید اپلیکیشن شما کند یا خراب به نظر می‌رسد. دلیلش این است که مرورگر باید برای هرکدام از آن عناصر، یک DOM element نگهداری کند و OWL هم باید همان‌ها را مدیریت کند.


روش‌های بهتر

  • صفحه‌بندی کنید: مثلاً هر بار ۵۰ یا ۸۰ رکورد نمایش دهید و اجازه دهید کاربر بین صفحات جابه‌جا شود.
  • دسته‌ای بارگذاری کنید: ابتدا بخش اول داده‌ها را دریافت کنید و سپس در صورت نیاز، بخش‌های بعدی را بارگذاری کنید.
  • قبل از رندر فیلتر کنید: فقط رکوردهایی را به template بدهید که کاربر واقعاً نیاز دارد ببیند.


مثال

<t t-foreach="visibleItems" t-as="item" t-key="item.id">
<div><t t-esc="item.name"/></div>
</t>

به visibleItems توجه کنید، نه کل array مربوط به items. منطق فیلتر و صفحه‌بندی در JavaScript قرار می‌گیرد، بنابراین template سریع و ساده باقی می‌ماند.


منطق را از Template خارج نگه دارید


Templateها هر بار که کامپوننت دوباره رندر می‌شود اجرا می‌شوند. اگر داخل یک template expression تابعی را فراخوانی کنید یا محاسبه‌ای انجام دهید، آن کار در هر به‌روزرسانی تکرار می‌شود؛ حتی اگر داده اصلی تغییر نکرده باشد. این یک افت عملکرد پنهان است که به‌راحتی ممکن است نادیده گرفته شود.


از چه چیزی باید اجتناب کرد؟

<t t-esc="calculateValue(item)"/>

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


به جای آن چه کار کنیم؟

مقادیر derived یا محاسبه‌شده را از قبل در JavaScript آماده کنید؛ ترجیحاً در willStart()، onWillUpdateProps() یا یک getter اختصاصی. سپس اجازه دهید template فقط نتیجه را بخواند:

this.processedItems = this.items.map(item => ({
...item,
displayValue: calculateValue(item),
}));

در این حالت template فقط مقدار processedItems.displayValue را می‌خواند. تمیز، سریع و قابل تست به‌صورت جداگانه.


ساختار کامپوننت‌ها


نحوه سازمان‌دهی کامپوننت‌ها فقط روی نگهداری کد اثر نمی‌گذارد؛ بلکه می‌تواند روی عملکرد هم اثر مستقیم داشته باشد. اگر یک کامپوننت بزرگ مسئول state یا props همه بخش‌ها باشد، هر زمان چیزی داخل آن تغییر کند، کل کامپوننت دوباره رندر می‌شود. بهتر است کامپوننت‌ها را تقسیم کنید تا فقط همان بخشی که تغییر کرده، خودش را به‌روزرسانی کند.


اصولی که ارزش رعایت دارند

  • هر کامپوننت باید یک کار را خوب انجام دهد.
  • هر جا ممکن است، کامپوننت‌ها را دوباره استفاده کنید و markup مشابه را در چند جا تکرار نکنید.
  • منطق template را در JavaScript نگه دارید و منطق رندر را در template.


مثال: یک کامپوننت کوچک و قابل استفاده مجدد به نام ProductCard

// ProductCard.js
export class ProductCard extends Component {
static template = 'my_module.ProductCard';
static props = {
name: String,
price: Number,
};
}
// ProductCard.xml
<t t-name="my_module.ProductCard">
<div class="product-card">
<span t-esc="props.name"/>
<span t-esc="props.price"/>
</div>
</t>
// Used in the parent template:
<ProductCard t-foreach="products" t-as="p"
t-key="p.id" name="p.name" price="p.price"/>

هر instance از ProductCard خودش را مدیریت می‌کند. به‌روزرسانی داده یک محصول فقط همان کارت را دوباره رندر می‌کند، نه کل لیست را.


استفاده درست از Serviceها


در اودوو، وجود service layer قطعاً هدف مشخصی دارد. این لایه به کاربران اجازه می‌دهد backend callها و منطق مشترک را متمرکز کنند، به جای اینکه یک کد مشابه را در چند object مختلف کپی کنند. یعنی اگر منطقی مشترک را در سه object مختلف تکرار می‌کنید، بهتر است آن منطق بخشی از یک service باشد.

Serviceها فقط کیفیت کد را بهتر نمی‌کنند، بلکه عملکرد را هم بهبود می‌دهند. برای مثال، یک service می‌تواند مکانیزم cache خودش را داشته باشد؛ یعنی درخواست‌های تکراری و غیرضروری به بک‌اند ارسال نمی‌کند.

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

مثال: استفاده از serviceهای orm و notification

import { useService } from '@web/core/utils/hooks';

export class MyComponent extends Component {
static template = 'my_module.MyComponent';

setup() {
this.orm = useService('orm');
this.notification = useService('notification');
this.state = useState({ records: [] });
}

async loadData() {
this.state.records = await this.orm.searchRead(
'sale.order',
[['state', '=', 'sale']],
['name', 'partner_id', 'amount_total']
);

this.notification.add('Data loaded successfully', {
type: 'success',
});
}
}


تکنیک‌های پیشرفته‌ای که ارزش دانستن دارند

وقتی اصول اولیه را رعایت کردید، چند تکنیک دیگر هم وجود دارد که در اپلیکیشن‌های بزرگ‌تر و پیچیده‌تر تفاوت قابل توجهی ایجاد می‌کنند.


Debounce کردن ورودی‌های جست‌وجو


اگر یک فیلد جست‌وجو با هر بار فشردن کلید، یک RPC call ارسال کند، سرور شما بی‌دلیل تحت فشار قرار می‌گیرد. Debounce منتظر می‌ماند تا کاربر تایپ را متوقف کند و بعد درخواست را ارسال می‌کند. همین تغییر ساده می‌تواند بار بک‌اند را به شکل چشمگیری کاهش دهد.

import { useService } from '@web/core/utils/hooks';
import { debounce } from '@web/core/utils/timing';

export class SearchComponent extends Component {
static template = 'my_module.SearchComponent';

setup() {
this.orm = useService('orm');
this.state = useState({ results: [] });

// Only fires 400ms after the user stops typing
this.onSearch = debounce(this._search.bind(this), 400);
}

async _search(query) {
this.state.results = await this.orm.searchRead(
'product.product',
[['name', 'ilike', query]],
['name', 'list_price']
);
}
}


اشتراک‌گذاری Module-Level Cache بین کامپوننت‌ها


برای داده‌های مرجع کاملاً static، مثل ارزها، کشورها یا واحدهای اندازه‌گیری، یک module-level cache باعث می‌شود داده فقط یک بار در هر session دریافت شود؛ مهم نیست چند کامپوننت به آن نیاز دارند.

// shared_cache.js
const _cache = {};

export async function getCachedData(orm, model, fields) {
if (!_cache[model]) {
_cache[model] = await orm.searchRead(model, [], fields);
}
return _cache[model];
}
// In any component that needs it:
import { getCachedData } from './shared_cache';

async willStart() {
this.state.currencies = await getCachedData(
this.orm, 'res.currency', ['name', 'symbol']
);
}


جمع‌بندی


OWL واقعاً خوب طراحی شده است و تقریباً همه بهبودهای عملکردی در آن به انجام همان چیزهایی برمی‌گردد که خود OWL پیشنهاد می‌کند: کار با state سبک، محاسبه داده‌ها قبل از رندر، ساخت کامپوننت‌های متمرکز و ارتباط مؤثر با بک‌اند. این بهبودها همیشه نیازمند بازنویسی کامل کد نیستند؛ بلکه اگر از ابتدا بهترین روش‌ها را رعایت کنید، بسیاری از مشکلات اصلاً ایجاد نمی‌شوند.

اگر هنگام توسعه فرانت‌اند اودوو با مشکلات عملکردی روبه‌رو شدید، یکی از موضوعات مطرح‌شده در این مقاله را انتخاب کنید و از همان نقطه تغییرات را شروع کنید. اغلب یک بهبود کوچک می‌تواند بسیار مؤثرتر از چیزی باشد که انتظار دارید.


درخواست مشاوره و دمو 

سؤالات متداول

در ۹ مورد از ۱۰ مورد، مشکل از state object است. اگر یک object یا array بزرگ را داخل useState ذخیره کنید و سپس propertyهای داخلی آن را تغییر دهید، OWL ممکن است تشخیص دهد که اتفاقی در حال رخ دادن است و در نتیجه کامپوننت را دوباره رندر کند.

برای بارگذاری اولیه داده‌هایی که template قبل از اولین نمایش به آن‌ها نیاز دارد، بله، willStart() معمولاً انتخاب مناسبی است. اما اگر داده باید هنگام تغییر props یا شرایط دیگر به‌روزرسانی شود، بهتر است از lifecycle hookهای مناسب‌تری مانند onWillUpdateProps() استفاده کنید.

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

چگونه عملکرد اپلیکیشن‌های OWL را در Odoo 19 بهینه کنیم؟
رهام ایزدی 1405/02/24
این پست را به اشتراک بگذارید
برچسب‌ها