بهبود تجربه اسکرول کردن در لیستهای خیلی بلند

بهبود تجربه اسکرول کردن در لیستهای خیلی بلند

اسکرول کردن جزئی جدانشدنی از تجربهٔ کاربر است. سعی کنید اینستاگرام یا توییتر را بدون Infinite Scrolling تصور کنید به طوری که کاربر ناچار باشد به عنوان مثال از صفحه‌بندی برای دیدن پست‌های صفحهٔ بعد یا قبل استفاده کند، قطعاً تجربهٔ خوشایندی نخواهد بود.

در علی‌بابا هم ما برای نمایش نتایج جستجو از Infinite Scrolling به جای صفحه‌بندی استفاده می‌کنیم. اما متأسفانه متوجه شدیم اسکرول کردن لیست نتایج، زمانی که تعداد نتایج زیاد باشد، نرم و روان نیست و تجربهٔ بسیار ناخوشایندی برای کاربر می‌سازد، به خصوص اگر کاربر از موبایل استفاده کند.

به طور کلی در تمام انیمیشن‌ها و حرکتهایی که کاربر در مرورگر می‌بیند، بسیار مهم است که Frame Rate یا آهنگ رندر کردن صفحه، از ۶۰ فریم بر ثانیه کمتر نباشد. با یک حساب سرانگشتی می‌توانیم بفهمیم که هر فریم فقط ۱۶ میلی‌ثانیه برای رندر شدن فرصت دارد. هر چیزی که باعث شود رندر شدن فریم‌ها بیشتر از این طول بکشد باعث می‌شود که انیمیشن یا حرکت اِلمان‌های مختلف در مرورگر غیرروان و با لَگ دیده شود و کاربر را آزار دهد.

اسکرول کردن و وارد شدن اِلمان‌های خارج از نمایشگر به داخل آن هم از این قاعده مستثنا نیست. اما در مورد سایت علی‌بابا چه چیزی باعث می‌شد وقتی که تعداد نتایج جستجو زیاد است رندر شدن هر فریم خیلی بیشتر از ۱۶ میلی‌ثانیه طول بکشد و تجربه کاربر در اسکرول کردن مختل شود؟

برای یافتن پاسخ باید دست به دامن پنل Performance در ابزار توسعهٔ کروم (Chrome DevTools) می‌شدیم.

استفاده از پنل Performance در DevTools برای یافتن ریشهٔ مشکل اسکرول

اولین گام در رفع هر مشکل پیدا کردن ریشهٔ آن است. پنل Performance در DevTools ابزاری است که با آن می‌توانیم سیر زمانی وقوع رویدادها در مرورگر و زمان لازم برای انجام محاسبات گوناگون را ثبت کنیم و روی نموداری که اصطلاحاً به آن Flame Graph می‌گویند ببینیم تا بتوانیم با تحلیل آن نمودار و داده‌ها، گلوگاه‌هایی را که مانع داشتن عملکرد خوب در صفحات وبسایت می‌شوند پیدا کنیم. در اینجا قصد نداریم شیوهٔ استفاده از این پنل را به طور کامل تشریح کنیم. برای این منظور می‌توانید به منابعی مثل این مراجعه کنید. در ادامه فقط اشاره می‌کنیم که چطور از این ابزار برای پیدا کردن و حل مشکل اسکرول کمک گرفتیم.

پس از آنکه صفحه نتایج جستجو را باز کردیم کافیست روی دکمه Record در پنل Performance کلیک کنیم و شروع به اسکرول کردن نتایج جستجو کنیم. هر بار به انتهای صفحه می‌رسیم، با یک درخواست Ajax، نتایج بیشتری از سمت سِرور دریافت می‌شوند. اسکرول را تا جایی ادامه می‌دهیم که عملکرد سایت به شدت اُفت کند، Frame Rate پایین بیاید و صفحه به سختی به تعاملات کاربر پاسخگو باشد. به این نقطه که رسیدیم دکمه Stop در پنل Performance را می‌فشاریم تا ضبط متوقف شود. در نتیجه نموداری شبیه این خواهیم داشت:

از این فاصله جزئیات زیادی در نمودار دیده نمی‌شود اما وقتی بزرگنمایی کنیم چنین چیزی خواهیم داشت:

این نمودار به ما می‌گوید زمانی که مشغول اسکرول کردن صفحه هستیم مجموعه‌ای از کارها برای رندر کردن دوباره صفحه در main thread یا همان رشته اصلی به صورت متوالی اتفاق می‌افتند و پشت سر هم تکرار می‌شوند. این کارها رشته اصلی را مسدود می‌کنند و اگر هرکدام از این کارها بیش از ۱۶ میلی‌ثانیه طول بکشد باعث تجربه ناخوشایند برای کاربر می‌شود. در واقع همانطور که این نمودار نشان می‌دهد همین اتفاق هم افتاده است و هربار انجام عملیات برای رندر صفحه حین اسکرول، چیزی حدود ۳۰ میلی‌ثانیه طول کشیده است که تقریبا دوبرابر مقدار ایده‌آل ما است.

همانطور که می‌توان در تصویر بالا دید، بیشتر زمانی که برای رندر دوباره صفحه صرف می‌شود به Update Layer Tree اختصاص دارد که با مستطیل بنفش مشخص شده است. فعلاً نگران این نباشید که Update Layer Tree چیست و چکار می‌کند، مهم این است که فعلاً ما گلوگاه پرفورمنسی خودمان را پیدا کرده‌ایم. مشکل ما Update Layer Tree است و اگر بتوانیم زمان این عملیات را کم کنیم بهبود قابل توجهی در اسکرول کردن خواهیم داشت.

نکته دیگری که می‌تواند در حل مشکل به ما کمک کند این است که زمان لازم برای انجام عملیات Update Layer Tree در ابتدا که حدود ۱۰۰ نتیجه برای جستجو از سمت سرور دریافت شده است حدود ۲۵ میلی‌ثانیه است اما هرچه بیشتر اسکرول کنیم و تعداد نتایج بیشتری در صفحه نشان داده شده باشند زمان لازم برای Update Layer Tree افزایش می‌یابد به طوری که بعد از حدود ۱۰ بار رسیدن به انتهای صفحه و دریافت ۱۰۰۰ نتیجه جستجو از سمت سرور، زمان لازم برای Update Layer Tree به حدود ۵۰۰ میلی‌ثانیه می‌رسد. یعنی هنگام اسکرول کردن، رشته اصلی نیم ثانیه مسدود می‌شود و در آن فاصله به برهم‌کنشهای کاربر پاسخگو نیست. نیم‌ثانیه فاصله پاسخگویی به کلیک یا هر برهم‌کنش دیگر کاربر اصلاً قابل قبول نیست.

یعنی زمان لازم برای Update Layer Tree با اندازه و تعداد اِلمان‌های موجود در DOM متناسب است و هرچه تعداد اِلمان‌هایی که باید در صفحه رندر شوند بیشتر شود این زمان هم افزایش می‌یابد.

خب حالا که ریشه مشکل را پیدا کردیم بهتر است ببینیم که اصلا Update Layer Tree چیست و چطور می‌توانیم زمان لازم برای آن را تا حد ممکن کم کنیم. برای این منظور باید نگاه کوتاهی به فرآیند رندر کردن در مرورگر بیندازیم.

هر فریم چطور در مرورگر رندر می‌شود؟

مرورگر برای تبدیل HTML، CSS و JavaScript به پیکسلهای روی صفحه نمایش، مجموعه‌ای از گامها را برمی‌دارد که به آن Critical Rendering Path می‌گویند.

منبع: https://developers.google.com/web/fundamentals/performance/rendering/simplify-paint-complexity-and-reduce-paint-areas

به طور خلاصه مرورگر با ترکیب DOM و CSSOM ساختاری درختی می‌سازد که به آن Render Tree می‌گویند.

این ساختار درختی شامل تمام اِلمان‌های مرئی در مرورگر و اطلاعات مربوط به نمایش آنهاست.

پس از به دست آوردن Render Tree نوبت به مرحله Layout می‌رسد. در این مرحله مرورگر با توجه به اندازه صفحه و اطلاعات Render Tree حساب می‌کند که هر اِلمان دقیقاً چه اندازه‌ای باید داشته باشد و باید در کجای صفحه قرار بگیرد.

در نهایت پس از مرحله Layout نوبت به Paint می‌رسد. در این مرحله مرورگر پیکسلهای مربوط به هر اِلمان را روی صفحه نمایش رسم می‌کند.

اما اینکه به ازای هر تغییر در Style و Layout، تمام عناصر صفحه از اول رسم شوند می‌تواند سنگین و زمان‌بر باشد. کاری که مرورگرها برای بهبود فرآیند Paint می‌کنند این است که تمام عناصر صفحه را به لایه‌های مجزا تقسیم می‌کنند که مانند تلق شفاف روی هم قرار گرفته‌اند. وقتی استایل یا چیدمان مربوط به عنصری از یک لایه تغییر کند فقط عناصر آن لایه تحت تأثیر قرار می‌گیرند بنابراین فقط برای آن لایه باید Paint اتفاق بیفتد.

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

اما در نهایت برای محاسبه آنچه که باید در صفحه نمایش دیده شود لازم است که پیکسلهای لایه‌های مختلف با هم ترکیب شوند و تصویر نهایی را به دست دهند. این اتفاق در مرحله Composite رخ می‌دهد. بد نیست بدانید Compositing یا ترکیب لایه‌های مختلف در GPU رخ می‌دهد نه CPU و این باعث می‌شود که CPU کمتر درگیر باشد و در نتیجه انیمیشن‌ها روان‌تر باشند.

از اینجا متوجه می‌شویم که بین زمان لازم برای Paint و زمان لازم برای Composite یکجور بده‌بستان یا trade-off وجود دارد به این معنی که ما برای کم کردن زمان Paint می‌توانیم تعداد لایه‌ها را زیاد کنیم اما در این صورت زمان لازم برای ترکیب لایه‌ها یا Composite افزایش می‌یابد و برعکس.

خب همهٔ این‌ها را گفتیم تا برسیم به اینجا که هربار کاربر اسکرول می‌کند مرورگر دوباره لایه‌ها را ترکیب می‌کند تا تصویر صفحه را به‌روز کند. Update Layer Tree هم بخشی از این فرآیند است که در آن حساب می‌شود که چه لایه‌هایی برای صفحه لازم هستند. زمانی که صرف Update Layer Tree می‌شود زمانی است که مرورگر صرف مدیریت لایه‌ها می‌کند و هرچه تعداد لایه‌های ما در صفحه بیشتر باشد این زمان هم بیشتر خواهد بود.

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

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

خب حالا به جایی رسیده‌ایم که می‌توانیم بگوییم مشکل را خوب تحلیل کرده‌ایم و به شناخت قابل قبولی از آن رسیده‌ایم. اما راه حل چیست؟ در ادامه خواهیم گفت.

DOM recycling و پیاده‌سازی آن با Vue.js

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

اما محدود کردن تعداد اِلمان‌های DOM کافی نیست. ما نمی‌خواهیم با هر اسکرول حتی همان تعداد محدود از اِلمان‌ها را هم هربار از نو بسازیم. راه حل آن است که ابتدا تعداد ثابتی اِلمان DOM را رندر کنیم. بعد هربار که یک اِلمان از بالای صفحه خارج می‌شود آن اِلمان را به پایین صفحه منتقل کنیم و محتوای آن را با محتوای آیتم جدیدی که در حال وارد شدن از پایین صفحه است به‌روز کنیم. به این ترتیب در هزینه هربار از بین بردن همهٔ آن ۵۰ اِلمان و ساختن دوبارهٔ آنها صرفه‌جویی کرده‌ایم.

این تکنیک استفاده از اِلمان‌های موجود برای رندر کردن آیتم‌های جدید، DOM recycling نام دارد و باید برای توسعه‌دهندگان اپلیکیشن‌های موبایل آشناتر باشد. در اندروید، RecyclerView و در React Native هم VirtualizedList از این تکنیک برای بهینه‌سازی اسکرول برای لیست‌های خیلی بلند استفاده می‌کنند.

اما در JavaScript و در Vue.js این رفتار چطور پیاده‌سازی می‌شود؟

ما برای پیاده‌سازی این رفتار یک کامپوننت اختصاصی برای رندر کردن لیستهای بلند در Vue.js پیاده کردیم. کد کامل این کامپوننت را می‌توانید اینجا ببینید. این کامپوننت ۱) فقط آیتم‌های قابل دیدن در صفحه نمایش را رندر می‌کند و ۲) از DOM recycling برای استفاده دوباره از اِلمان‌های DOM که از صفحه خارج شده‌اند کمک می‌گیرد.

برای توسعه این کامپوننت از دو منبع اصلی کمک گرفتیم.

اولین منبع ما این مقاله بود که در آن درباره پیچیدگی یک infinite scroller و پیاده‌سازی آن در JavaScript با استفاده از DOM recycling توضیح داده شده است.

و منبع دوم یک کامپوننت Vue.js به اسم vue-virtual-scroller بود که برای همین کار توسعه داده شده بود. اما برخی قسمت‌های این کامپوننت مناسب استفادهٔ ما نبود و دوست داشتیم برخی بخش‌های آن طور دیگری پیاده شده باشند به همین دلیل ترجیح دادیم کامپوننت خودمان را با کمک این دو منبع بسازیم.

در ادامه به طور خلاصه به شیوه عملکرد این کامپوننت می‌پردازیم.

کد زیر تمپلیت این کامپوننت را نشان می‌دهد:

<template>
    <div
        class="listview"
        :class="{ ready }"
        :style="{ height: totalHeight !== null ? `${totalHeight}px` : null }"
    >
        <div
            v-for="view in pool"
            class="listview__item"
            :key="view.nr.id"
            :style="ready ? { transform: `translateY(${view.position}px)` } : null"
            :ref="`listviewItem`"
        >
            <slot
                :item="view.item"
                :index="view.nr.index"
                :active="view.nr.used"
            />
        </div>
    </div>
</template>

در صورتی که همهٔ آیتمهای لیستمان را صرف نظر از اینکه در صفحه نمایش باشند یا نباشند رندر می‌کردیم طول (height) کامپوننت ما به طور خودکار تنظیم می‌شد. اما از آنجا که ما فقط آیتم‌هایی از لیست را رندر می‌کنیم که در صفحه نمایش قابل دیدن هستند پس لازم است طول کامپوننت را خودمان محاسبه کنیم تا جای خالی آیتم‌های رندر نشده را پر کنیم. ما مقدار این ارتفاع را با totalHeight نشان داده‌ایم. پس بخش اول کار، محاسبهٔ این مقدار است.

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

متد زیر نشان می‌دهد که طول کل کامپوننت چطور محاسبه می‌شود:

calculateTotalHeight () {
    const keyField = this.keyField;
    
    if (this.itemSize) {
        return this.itemSize * this.items.length;
    }

    let height = 0;

    for (let i = 0; i < this.items.length; i++) {
        const key = keyField ? this.items[i][keyField] : this.items[i];
        height += this.sizeCache.get(key) || this.averageItemSize || 0;
    }

    return height;
}

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

measureItems () {
    const poolDomElements = this.$refs.listviewItem;
    let hasUpdated = false;

    poolDomElements.forEach((poolDomElement, index) => {
    const view = this.pool[index];

        if (view.nr.used) {
            const height = poolDomElement.offsetHeight;
            const key = view.nr.key;
            if (!this.sizeCache.has(key) || this.sizeCache.get(key) !== height) {
                hasUpdated = true;
                this.sizeCache.set(key, height);
            }
        }
    });

    if (hasUpdated) {
        const sizesCount = this.sizeCache.size;
        let sizesSum = 0;

        this.sizeCache.forEach((size) => {
            sizesSum += size;
        });

        const averageItemSize = sizesSum / sizesCount;

        this.averageItemSize = averageItemSize;
    }
}

پس از محاسبه طول کل کامپوننت نوبت به رندر کردن آیتم‌هایی می‌رسد که در صفحه نمایش دیده می‌شوند. این که چه آیتم‌هایی در صفحه نمایش دیده می‌شوند به طول صفحه نمایش و این که کاربر چقدر اسکرول کرده است  بستگی دارد. هرچه کاربر بیشتر اسکرول کند باید آیتم‌هایی را ببیند که به انتهای لیست نزدیک‌تر اند. همان طور که در تمپلیت کامپوننت می‌توانید ببینید ما برای تنظیم مکان هر آیتم از transform و translateY استفاده کردیم. استفاده از transform برای یک آیتم باعث می‌شود مرورگر برای آن آیتم یک لایه جداگانه بسازد تا تغییر مکان آن آیتم باعث Paint نشود و فقط باعث Composite شود. در نتیجه جابه‌جا شدن آیتم‌های گوناگون فقط GPU را درگیر خواهد کرد.

ما برای آن که ببینیم کاربر چقدر اسکرول کرده است نزدیک‌ترین اِلمان قابل اسکرول به کامپوننت خودمان را پیدا می‌کنیم و میزان اسکرول را برای آن اِلمان اندازه می‌گیریم. تابع زیر به ما کمک می‌کند نزدیک‌ترین اِلمان قابل اسکرول به یک node را پیدا کنیم:

function getFirstScrollableParent (node) {
    const parentNode = node && node.parentNode;
    if (!parentNode || parentNode === document.body) {
        return document.body;
    } else if (isScrollable(parentNode)) {
        return parentNode;
    } else {
        return getFirstScrollableParent(parentNode);
    }
}

function isScrollable (node) {
    const scrollRegex = /(auto|scroll)/;

    function getStyle (node, prop) {
        getComputedStyle(node, null).getPropertyValue(prop);
    }

    return (
        scrollRegex.test(getStyle(node, 'overflow')) ||
        scrollRegex.test(getStyle(node, 'overflow-x')) ||
        scrollRegex.test(getStyle(node, 'overflow-y'))
    );
}

در نهایت ما با استفاده از میزان اسکرول دو مقدار firstAttachedItem و lastAttachedItem را به دست می‌آوریم که مقدار آنها برابر است با index اولین و آخرین آیتمی که باید در صفحه نمایش رندر شوند. هر بار کاربر اسکرول می کند این مقادیر از نو حساب می‌شوند و آیتم‌های جدید در صفحه نمایش نشان داده می‌شوند.

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

برای این کار ما یک آرایه از اشیاء داریم که هر عضو آن نشان‌دهنده یک اِلمان DOM است که در مرورگر رندر شده است. این آرایه را pool نامیده‌ایم. هریک از اعضای این آرایه می‌تواند یکی از این دو وضعیت را داشته باشد. یا نشان‌دهنده آیتمی است که در محدوده صفحه نمایش است و به عبارت دیگر استفاده شده است. یا نشان‌دهنده آیتمی است که از محدوده صفحه نمایش خارج شده است و می‌تواند برای نمایش آیتم‌هایی که در حال ورود به صفحه نمایش هستند مجدداً مورد استفاده قرار بگیرد.

هربار کاربر اسکرول می‌کند ما firstAttachedItem و lastAttachedItem را به دست می‌آوریم و بعد تک تک اعضای آرایهٔ pool را بررسی می‌کنیم. اگر عضو آرایه نشان‌دهنده آیتمی بود که در بین firstAttachedItem و lastAttachedItem قرار داشت وضعیت آن عضو را تغییر نمی‌دهیم و فقط موقعیتش را دوباره تنظیم می‌کنیم. اما اگر عضو آرایه، نشان‌دهنده آیتمی بود که از محدوده صفحه نمایش خارج شده است وضعیت آن عضو را به unused تغییر می‌دهیم و موقعیت آن را برابر با -9999px می‌گذاریم تا اِلمان DOM مربوط به آن در صفحه نمایش کاربر دیده نشود.

درنهایت برای نمایش آیتم‌هایی که در محدوده صفحه نمایش هستند یک حلقه for از firstAttachedItem تا lastAttachedItem اجرا می‌کنیم. اگر برای آیتم مورد نظر از قبل یک عضو متناظر در pool وجود داشت فقط مختصات مکانی آن عضو را تنظیم می‌کنیم. اگر وجود نداشت و عضوهای استفاده نشده‌ای در pool وجود داشتند یکی از عضوهای استفاده نشده را برمی‌داریم و آیتم و موقعیت جدید را برای آن تنظیم می‌کنیم. و اگر هیچ عضو استفاده نشده‌ای در pool وجود نداشت یه عضو جدید به pool اضافه می‌کنیم.

کد زیر نشان می‌دهد چطور پس از آنکه به هر آیتم بین firstAttachedItem و lastAttachedItem یک عضو pool اختصاص دادیم مختصات مکانی هرکدام از آیتم‌ها را تنظیم می‌کنیم:

for (let i = this.firstAttachedItem; i <= this.lastAttachedItem; i++) {
    const key = keyField ? this.items[i][keyField] : this.items[i];
    const view = this.views.get(key);
    view.position = curPos;
    curPos += this.sizeCache.get(key) || this.averageItemSize || 0;
}

به این ترتیب کامپوننت ما برای بهبود تجربه اسکرول کاربر، فقط به تعداد لازم اِلمان DOM در صفحه می‌سازد و با استفاده از DOM recycling از اِلمان‌های بی‌استفاده برای نمایش آیتم‌هایی که به صفحه وارد می‌شوند استفاده می‌کند.

نتیجه

حال پس از آنکه کامپوننت خودمان را ساختیم وقت آن است که ببینیم آیا با این کامپوننت مشکل اسکرول کردن کاربر حل می‌شود و آیا تجربه نرم‌تر و روان‌تری در اسکرول کردن صفحه پیدا می‌کند؟

تصویر زیر لایه‌های صفحه نتایج علی‌بابا را پس از استفاده از این کامپوننت نشان می‌دهد:

همانطور که می‌بینید تعداد لایه‌ها در مقایسه با قبل بسیار کمتر شده است و فقط به آیتم‌هایی که در محدوده صفحه نمایش هستند رندر شده‌اند.

وقتی از صفحهٔ نتایج با استفاده از پنل Performance هنگام اسکرول کردن پروفایل بگیریم نتیجه زیر به دست می‌آید:

همانطور که می‌توان دید زمان لازم برای Update Layer Tree حتی پس از آنکه حدود ۱۰۰۰ نتیجه از سِرور دریافت شده‌اند، از حدود ۵۰۰ میلی‌ثانیه به کمتر از ۱۰ میلی‌ثانیه رسیده است و مقدار آن مستقل از تعداد نتایج دریافت شده از سمت سرور است چون فقط به ازای تعداد محدودی از کل نتایج اِلمان DOM در صفحه وجود دارد.