تکنیک‌هایی برای بارگیری سریع یک برنامه وب، حتی در تلفن همراه

چگونه از تقسیم کد، درون‌سازی کد و رندر سمت سرور در PROXX استفاده کردیم.

در Google I/O 2019، ماریکو، جیک و من PROXX را ارسال کردیم، یک مین‌روب مدرن برای وب. چیزی که PROXX را متمایز می کند تمرکز بر دسترسی (شما می توانید آن را با یک صفحه خوان بازی کنید!) و توانایی اجرای آن روی یک تلفن همراه مانند یک دستگاه رومیزی پیشرفته است. تلفن های دارای ویژگی به چند روش محدود می شوند:

  • CPU های ضعیف
  • پردازنده‌های گرافیکی ضعیف یا موجود نیستند
  • صفحه نمایش های کوچک بدون ورودی لمسی
  • حافظه بسیار محدود

اما آنها یک مرورگر مدرن اجرا می کنند و بسیار مقرون به صرفه هستند. به همین دلیل، تلفن های همراه در بازارهای نوظهور دوباره احیا می کنند. قیمت آنها به مخاطبان کاملاً جدیدی که قبلاً توانایی پرداخت آن را نداشتند، اجازه می دهد تا آنلاین شوند و از وب مدرن استفاده کنند. برای سال 2019 پیش‌بینی می‌شود که حدود 400 میلیون تلفن هوشمند تنها در هند فروخته شود ، بنابراین کاربران تلفن‌های ویژگی ممکن است بخش قابل توجهی از مخاطبان شما باشند. علاوه بر آن، سرعت اتصال مشابه 2G در بازارهای در حال ظهور عادی است. چگونه توانستیم PROXX را تحت شرایط تلفن همراه به خوبی کار کنیم؟

گیم پلی PROXX.

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

این قسمت 1 از یک مجموعه دو قسمتی است. بخش 1 بر عملکرد بارگذاری تمرکز دارد و بخش 2 بر عملکرد زمان اجرا تمرکز خواهد کرد.

تسخیر وضعیت موجود

آزمایش عملکرد بارگیری خود در یک دستگاه واقعی بسیار مهم است. اگر دستگاه واقعی در دسترس ندارید، من WebPageTest را توصیه می‌کنم، مخصوصاً راه‌اندازی «ساده» را . WPT یک باتری از تست های بارگیری را روی یک دستگاه واقعی با اتصال 3G شبیه سازی شده اجرا می کند.

3G سرعت خوبی برای اندازه گیری است. در حالی که ممکن است به 4G، LTE یا به زودی حتی 5G عادت کرده باشید، واقعیت اینترنت موبایل کاملاً متفاوت به نظر می رسد. شاید در قطار، کنفرانس، کنسرت یا پرواز هستید. آنچه شما در آنجا تجربه خواهید کرد به احتمال زیاد به 3G نزدیکتر است و گاهی اوقات حتی بدتر.

همانطور که گفته شد، ما در این مقاله بر روی 2G تمرکز خواهیم کرد زیرا PROXX به صراحت تلفن های همراه و بازارهای نوظهور را در مخاطبان هدف خود هدف قرار می دهد. هنگامی که WebPageTest آزمایش خود را انجام داد، یک آبشار (مشابه آنچه در DevTools می بینید) و همچنین یک نوار فیلم در بالا دریافت می کنید. نوار فیلم نشان می دهد که کاربر شما هنگام بارگیری برنامه شما چه می بیند. در 2G، تجربه بارگیری نسخه بهینه نشده PROXX بسیار بد است:

ویدیوی نوار فیلم نشان می دهد که کاربر هنگام بارگیری PROXX بر روی یک دستگاه واقعی و ارزان قیمت از طریق اتصال 2G شبیه سازی شده، چه می بیند.

وقتی کاربر از طریق 3G بارگذاری می شود، 4 ثانیه عدم وجود سفید را می بیند. بیش از 2G کاربر برای بیش از 8 ثانیه مطلقاً چیزی نمی بیند. اگر دلیل اهمیت عملکرد را می خوانید، می دانید که ما اکنون بخش خوبی از کاربران بالقوه خود را به دلیل بی حوصلگی از دست داده ایم. کاربر باید تمام 62 کیلوبایت جاوا اسکریپت را دانلود کند تا هر چیزی روی صفحه نمایش داده شود. پوشش نقره ای در این سناریو این است که دومین چیزی که روی صفحه ظاهر می شود، تعاملی است. یا آن؟

[First Meaningful Paint][FMP] در نسخه بهینه نشده PROXX از نظر فنی [تعاملی][TTI] است اما برای کاربر بی فایده است.

پس از دانلود حدود 62 کیلوبایت gzip'd JS و ایجاد DOM، کاربر برنامه ما را مشاهده می کند. این برنامه از نظر فنی تعاملی است. با این حال، نگاه به تصویر، واقعیت دیگری را نشان می دهد. فونت‌های وب همچنان در پس‌زمینه بارگذاری می‌شوند و تا زمانی که آماده شوند، کاربر نمی‌تواند متنی را ببیند. در حالی که این حالت به عنوان اولین رنگ معنی دار (FMP) واجد شرایط است، مطمئناً واجد شرایط تعامل مناسب نیست، زیرا کاربر نمی تواند بگوید که هر یک از ورودی ها در مورد چیست. یک ثانیه دیگر در 3G و 3 ثانیه در 2G طول می کشد تا برنامه آماده اجرا شود. در مجموع، این برنامه 6 ثانیه در 3G و 11 ثانیه در 2G طول می کشد تا تعاملی شود.

تجزیه و تحلیل آبشار

اکنون که می دانیم کاربر چه چیزی را می بیند، باید دلیل آن را بفهمیم. برای این ما می توانیم به آبشار نگاه کنیم و تحلیل کنیم که چرا منابع خیلی دیر بار می شوند. در ردیابی 2G ما برای PROXX می توانیم دو پرچم قرمز اصلی را ببینیم:

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

کاهش تعداد اتصالات

هر خط نازک ( dns , connect , ssl ) مخفف ایجاد یک اتصال HTTP جدید است. راه اندازی یک اتصال جدید پرهزینه است زیرا در 3G حدود 1 ثانیه و در 2G تقریباً 2.5 ثانیه طول می کشد. در آبشار ما یک اتصال جدید برای:

  • درخواست شماره 1: index.html ما
  • درخواست شماره 5: سبک های فونت از fonts.googleapis.com
  • درخواست شماره 8: Google Analytics
  • درخواست شماره 9: یک فایل فونت از fonts.gstatic.com
  • درخواست شماره 14: مانیفست برنامه وب

اتصال جدید برای index.html اجتناب ناپذیر است. مرورگر باید یک اتصال به سرور ما ایجاد کند تا محتویات را دریافت کند. اتصال جدید برای Google Analytics را می‌توان با وارد کردن چیزی مانند Minimal Analytics اجتناب کرد، اما Google Analytics مانع از ارائه یا تعاملی شدن برنامه ما نمی‌شود، بنابراین ما واقعاً به سرعت بارگیری آن اهمیت نمی‌دهیم. در حالت ایده آل، گوگل آنالیتیکس باید در زمان بیکاری بارگیری شود، زمانی که همه چیز قبلاً بارگیری شده باشد. به این ترتیب در طول بارگذاری اولیه، پهنای باند یا توان پردازشی را نمی گیرد. اتصال جدید برای مانیفست برنامه وب توسط مشخصات واکشی تجویز شده است، زیرا مانیفست باید از طریق یک اتصال غیرمجاز بارگیری شود. باز هم، مانیفست برنامه وب، برنامه ما را از رندر یا تعاملی شدن مسدود نمی کند، بنابراین نیازی به اهمیت چندانی نداریم.

با این حال، دو فونت و سبک آنها مشکل ساز هستند زیرا رندر و همچنین تعامل را مسدود می کنند. اگر به CSS ارائه شده توسط fonts.googleapis.com نگاه کنیم، این فقط دو قانون @font-face است، یکی برای هر فونت. سبک های فونت در واقع آنقدر کوچک هستند که تصمیم گرفتیم آن را در HTML خود قرار دهیم و یک اتصال غیر ضروری را حذف کنیم. برای جلوگیری از هزینه راه‌اندازی اتصال برای فایل‌های فونت، می‌توانیم آنها را در سرور خود کپی کنیم.

موازی کردن بارها

با نگاهی به آبشار، می بینیم که پس از بارگذاری اولین فایل جاوا اسکریپت، فایل های جدید بلافاصله بارگذاری می شوند. این برای وابستگی های ماژول معمولی است. ماژول اصلی ما احتمالا دارای واردات ثابت است، بنابراین جاوا اسکریپت نمی تواند تا زمانی که آن واردات بارگذاری شود اجرا شود. نکته مهمی که در اینجا باید بدانیم این است که این نوع وابستگی ها در زمان ساخت شناخته می شوند. می‌توانیم از تگ‌های <link rel="preload"> استفاده کنیم تا مطمئن شویم که همه وابستگی‌ها در ثانیه‌ای که HTML خود را دریافت می‌کنیم شروع به بارگیری می‌کنند.

نتایج

بیایید نگاهی بیندازیم که تغییرات ما چه دستاوردهایی داشته است. مهم است که هیچ متغیر دیگری را در تنظیمات تست خود تغییر ندهید که می تواند نتایج را تغییر دهد، بنابراین ما از تنظیمات ساده WebPageTest برای بقیه این مقاله استفاده می کنیم و به نوار فیلم نگاه می کنیم:

ما از نوار فیلم WebPageTest استفاده می کنیم تا ببینیم تغییرات ما به چه چیزی رسیده است.

این تغییرات TTI ما را از 11 به 8.5 کاهش داد ، که تقریباً 2.5 ثانیه زمان تنظیم اتصال است که ما قصد حذف آن را داشتیم. آفرین به ما

پیش اجرا

در حالی که ما فقط TTI خود را کاهش دادیم، اما واقعاً روی صفحه سفید طولانی و ابدی که کاربر باید برای 8.5 ثانیه تحمل کند، تأثیری نداشته است. مسلماً بزرگترین پیشرفت ها برای FMP را می توان با ارسال نشانه گذاری سبک در index.html خود به دست آورد . تکنیک‌های رایج برای دستیابی به این هدف، رندر از قبل و رندر سمت سرور است که ارتباط نزدیکی با هم دارند و در Rendering در وب توضیح داده شده‌اند. هر دو تکنیک برنامه وب را در Node اجرا می کنند و DOM حاصل را به HTML سریال می کنند. رندر سمت سرور این کار را به ازای هر درخواست در سمت سرور انجام می دهد، در حالی که پیش رندر این کار را در زمان ساخت انجام می دهد و خروجی را به عنوان index.html جدید شما ذخیره می کند. از آنجایی که PROXX یک برنامه JAMStack است و هیچ سمت سروری ندارد، تصمیم گرفتیم پیش اجرا را اجرا کنیم.

راه های زیادی برای پیاده سازی پیش اجرا وجود دارد. در PROXX ما استفاده از Puppeteer را انتخاب کردیم که Chrome را بدون هیچ رابط کاربری راه‌اندازی می‌کند و به شما امکان می‌دهد آن نمونه را با یک Node API از راه دور کنترل کنید. ما از این برای تزریق نشانه گذاری و جاوا اسکریپت خود استفاده می کنیم و سپس DOM را به عنوان رشته ای از HTML بازخوانی می کنیم. از آنجایی که ما از ماژول‌های CSS استفاده می‌کنیم، سبک‌هایی را که به آن نیاز داریم به صورت رایگان در قالب CSS دریافت می‌کنیم.

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(rawIndexHTML);
  await page.evaluate(codeToRun);
  const renderedHTML = await page.content();
  browser.close();
  await writeFile("index.html", renderedHTML);

با وجود این، می‌توانیم انتظار بهبودی برای FMP خود داشته باشیم. ما هنوز باید همان مقدار جاوا اسکریپت قبلی را بارگذاری و اجرا کنیم، بنابراین نباید انتظار داشته باشیم که TTI تغییر زیادی کند. در هر صورت، index.html ما بزرگتر شده است و ممکن است کمی TTI ما را به عقب براند. تنها یک راه برای پیدا کردن وجود دارد: اجرای WebPageTest.

نوار فیلم بهبود واضحی را برای متریک FMP ما نشان می دهد. TTI عمدتاً تحت تأثیر قرار نمی گیرد.

اولین رنگ معنادار ما از 8.5 ثانیه به 4.9 ثانیه رسیده است که یک پیشرفت عظیم است. TTI ما هنوز در حدود 8.5 ثانیه اتفاق می افتد، بنابراین تا حد زیادی تحت تأثیر این تغییر قرار نگرفته است. کاری که ما اینجا انجام دادیم یک تغییر ادراکی است. برخی حتی ممکن است آن را یک اهمال کاری خطاب کنند. با ارائه تصویری متوسط ​​از بازی، عملکرد بارگذاری درک شده را برای بهتر شدن تغییر می دهیم.

خط کشی

معیار دیگری که هم DevTools و هم WebPageTest به ما می دهند، Time To First Byte (TTFB) است. این مدت زمانی است که از اولین بایت درخواست ارسال شده تا اولین بایت پاسخ دریافت شده طول می کشد. این زمان اغلب یک زمان رفت و برگشت (RTT) نیز نامیده می شود، اگرچه از نظر فنی بین این دو عدد تفاوت وجود دارد: RTT شامل زمان پردازش درخواست در سمت سرور نمی شود. DevTools و WebPageTest TTFB را با رنگ روشن در بلوک درخواست/پاسخ تجسم می کنند.

بخش نور یک درخواست نشان می دهد که درخواست در انتظار دریافت اولین بایت پاسخ است.

با نگاهی به آبشار خود، می‌توانیم ببینیم که همه درخواست‌ها بیشتر وقت خود را صرف انتظار برای رسیدن اولین بایت پاسخ می‌کنند.

این مشکل همان چیزی بود که HTTP/2 Push در ابتدا برای آن طراحی شد. توسعه‌دهنده برنامه می‌داند که منابع خاصی مورد نیاز است و می‌تواند آنها را به سمت پایین بکشاند . زمانی که کلاینت متوجه می شود که باید منابع اضافی را واکشی کند، آنها در حال حاضر در حافظه پنهان مرورگر هستند. HTTP/2 Push برای درست کردن کار خیلی سختی بود و دلسرد تلقی می‌شود. این فضای مشکل در طول استانداردسازی HTTP/3 مجدداً بررسی خواهد شد. در حال حاضر، ساده‌ترین راه‌حل این است که تمام منابع حیاتی را به قیمت بازده ذخیره‌سازی درون‌بندی کنیم .

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

با جاوا اسکریپت خود، TTI خود را از 8.5 به 7.2 کاهش دادیم.

این 1 ثانیه از TTI ما کم کرد. اکنون به نقطه‌ای رسیده‌ایم که index.html ما حاوی هر چیزی است که برای رندر اولیه و تعاملی شدن لازم است. HTML می تواند در حالی که هنوز در حال بارگیری است رندر شود و FMP ما را ایجاد کند. لحظه ای که HTML تجزیه و اجرا می شود، برنامه تعاملی است.

تقسیم کد تهاجمی

بله، index.html ما شامل همه چیزهایی است که برای تعاملی شدن لازم است. اما با بررسی دقیق‌تر معلوم می‌شود که شامل هر چیز دیگری نیز می‌شود. index.html ما حدود 43 کیلوبایت است. بیایید آن را در رابطه با آنچه کاربر می‌تواند در ابتدا با آن تعامل داشته باشد، در نظر بگیریم: ما یک فرم برای پیکربندی بازی داریم که شامل چند مؤلفه، یک دکمه شروع و احتمالاً مقداری کد برای تداوم و بارگیری تنظیمات کاربر است. تقریباً همین است. 43 کیلوبایت زیاد به نظر می رسد.

صفحه فرود PROXX. در اینجا فقط از اجزای حیاتی استفاده می شود.

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

تجزیه و تحلیل محتویات «index.html» PROXX منابع غیر ضروری زیادی را نشان می دهد. منابع حیاتی برجسته می شوند.

کاری که ما باید انجام دهیم تقسیم کد است. تقسیم کد، بسته یکپارچه شما را به قطعات کوچک‌تری تقسیم می‌کند که می‌توانند در صورت تقاضا بارگذاری شوند. باندلرهای محبوب مانند Webpack ، Rollup و Parcel از تقسیم کد با استفاده از import() پویا پشتیبانی می‌کنند. باندلر کد شما را تجزیه و تحلیل می‌کند و همه ماژول‌هایی را که به صورت ایستا وارد می‌شوند، درون خط می‌کند . هر چیزی که به صورت پویا وارد می‌کنید در فایل خودش قرار می‌گیرد و تنها زمانی که فراخوانی import() اجرا شود، از شبکه واکشی می‌شود. البته ضربه زدن به شبکه هزینه دارد و تنها در صورتی باید انجام شود که وقت کافی داشته باشید. مانترا در اینجا این است که ماژول هایی را که در زمان بارگذاری به شدت مورد نیاز هستند وارد کنیم و هر چیز دیگری را به صورت پویا بارگذاری کنیم. اما نباید تا آخرین لحظه منتظر ماژول‌های تنبلی باشید که قطعاً استفاده می‌شوند. فیلم Idle Until Urgent از Phil Walton یک الگوی عالی برای یک میانه سالم بین بارگیری تنبل و بارگیری مشتاق است.

در PROXX ما یک فایل lazy.js ایجاد کردیم که به صورت ایستا هر چیزی را که به آن نیاز نداریم وارد می کند. در فایل اصلی خود، می توانیم lazy.js به صورت پویا وارد کنیم. با این حال، برخی از مؤلفه‌های Preact ما به lazy.js ختم شدند، که معلوم شد کمی پیچیده است زیرا Preact نمی‌تواند مؤلفه‌های تنبل بارگذاری شده را خارج از جعبه کنترل کند. به همین دلیل، ما یک بسته بندی کامپوننت deferred کوچک نوشتیم که به ما اجازه می دهد تا زمانی که کامپوننت واقعی بارگذاری شود، یک مکان نگهدار را ارائه کنیم.

export default function deferred(componentPromise) {
  return class Deferred extends Component {
    constructor(props) {
      super(props);
      this.state = {
        LoadedComponent: undefined
      };
      componentPromise.then(component => {
        this.setState({ LoadedComponent: component });
      });
    }

    render({ loaded, loading }, { LoadedComponent }) {
      if (LoadedComponent) {
        return loaded(LoadedComponent);
      }
      return loading();
    }
  };
}

با وجود این، می توانیم از Promise یک جزء در توابع render() خود استفاده کنیم. برای مثال، مؤلفه <Nebula> ، که تصویر پس‌زمینه متحرک را ارائه می‌کند، در حین بارگیری مؤلفه با یک <div> خالی جایگزین می‌شود. هنگامی که کامپوننت بارگیری شد و آماده استفاده شد، <div> با کامپوننت واقعی جایگزین می شود.

const NebulaDeferred = deferred(
  import("/components/nebula").then(m => m.default)
);

return (
  // ...
  <NebulaDeferred
    loading={() => <div />}
    loaded={Nebula => <Nebula />}
  />
);

با همه این موارد، index.html خود را به 20 کیلوبایت کاهش دادیم، کمتر از نیمی از اندازه اصلی. این چه تأثیری بر FMP و TTI دارد؟ WebPageTest خواهد گفت!

نوار فیلم تایید می کند: TTI ما اکنون در 5.4 ثانیه است. یک پیشرفت چشمگیر نسبت به 11 های اصلی ما.

FMP و TTI ما فقط 100 میلی‌ثانیه از هم فاصله دارند، زیرا فقط بحث تجزیه و اجرای جاوا اسکریپت خطی است. پس از تنها 5.4 ثانیه در 2G، برنامه کاملاً تعاملی است. همه ماژول‌های کمتر ضروری دیگر در پس‌زمینه بارگذاری می‌شوند.

Sleight of Hand بیشتر

اگر به لیست ماژول های حیاتی بالا نگاه کنید، خواهید دید که موتور رندر بخشی از ماژول های بحرانی نیست. البته تا زمانی که موتور رندر خود را برای رندر گرفتن بازی نداشته باشیم، بازی نمی تواند شروع شود. تا زمانی که موتور رندر ما برای شروع بازی آماده شود، می‌توانیم دکمه «شروع» را غیرفعال کنیم، اما طبق تجربه ما، کاربر معمولاً به اندازه‌ای طول می‌کشد تا تنظیمات بازی خود را پیکربندی کند که این کار ضروری نیست. اکثر اوقات موتور رندر و سایر ماژول های باقیمانده با فشار دادن "شروع" توسط کاربر بارگذاری می شوند. در موارد نادری که کاربر سریعتر از اتصال شبکه خود است، یک صفحه بارگیری ساده را نشان می دهیم که منتظر می ماند تا ماژول های باقی مانده تمام شوند.

نتیجه

اندازه گیری مهم است. برای جلوگیری از صرف زمان برای مشکلاتی که واقعی نیستند، توصیه می کنیم همیشه قبل از اجرای بهینه سازی ابتدا اندازه گیری کنید. علاوه بر این، اگر دستگاه واقعی در دسترس نباشد، اندازه‌گیری‌ها باید روی دستگاه‌های واقعی در اتصال 3G یا در WebPageTest انجام شود.

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

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

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