Özellikli telefonlarda bile bir web uygulamasını hızlı yükleme teknikleri

PROXX'te kod bölme, kod satır içine alma ve sunucu tarafı oluşturmayı nasıl kullandık?

Google I/O 2019'da Mariko, Jake ve ben web için modern bir Mayın Tarlası klonu olan PROXX'u gönderdik. PROXX'u diğerlerinden ayıran şeylerden biri erişilebilirliğe odaklanmak (ekran okuyucuyla oynayabilirsiniz!) ve özellikli telefonlarda, ileri teknoloji masaüstü cihazlarda olduğu gibi çalışabilme imkanıdır. Özellikli telefonlar çeşitli şekillerde kısıtlanır:

  • Zayıf CPU'lar
  • Zayıf veya var olmayan GPU'lar
  • Dokunmatik girişi olmayan küçük ekranlar
  • Çok sınırlı miktarda bellek

Ancak modern bir tarayıcıya sahipler ve oldukça uygun fiyatlılar. Bu nedenle, özellikli telefonlar gelişmekte olan pazarlarda yeniden canlanıyor. Fiyat noktaları, daha önce bütçelerinden yararlanmayan yeni bir kitleye ulaşmalarına ve internet ortamına katılarak modern web'den yararlanmasına olanak tanıyor. 2019'da yalnızca Hindistan'da yaklaşık 400 milyon özellikli telefon satılacağı tahmin ediliyor. Bu nedenle, özellikli telefon kullanıcılarının kitlenizin önemli bir bölümünü oluşturması mümkün olabilir. Ayrıca 2G'ye benzer bağlantı hızları gelişmekte olan pazarlarda normdur. Özellikli telefon koşullarında PROXX'un iyi çalışmasını nasıl sağladık?

PROXX oyunu.

Performans önemlidir ve hem yükleme performansını hem de çalışma zamanı performansını içerir. İyi performansın; kullanıcıları elde tutmada artış, daha iyi dönüşümler ve en önemlisi de daha fazla kapsayıcılık ile ilişkili olduğu gösterilmiştir. Jeremy Wagner'de performansın neden önemli olduğu ile ilgili daha fazla veri ve bilgi bulabilirsiniz.

Bu, iki bölümden oluşan bir dizinin 1. bölümüdür. 1. bölüm yükleme performansına, 2. bölüm ise çalışma zamanı performansına odaklanıyor.

Mevcut durumu değerlendirme

Yükleme performansınızı gerçek bir cihazda test etmek son derece önemlidir. Gerçek bir cihazınız yoksa WebPageTest'i, özellikle de "basit" kurulumu yapmanızı öneririz. WPT, emüle edilmiş 3G bağlantısı olan gerçek bir cihazda yüklü testler çalıştırır.

3G, ölçüm için iyi bir hızdır. 4G'ye, LTE'ye, hatta yakında 5G'ye alışkın olsanız da mobil internet gerçekliği bambaşka görünüyor. Trende, konferansta, konserde veya uçakta olabilirsiniz. Buralarda yaşayacağınız deneyim, büyük olasılıkla 3G'ye daha yakın ve bazen daha kötü.

Bununla birlikte, PROXX açık bir şekilde hedef kitlesinde özellikli telefonları ve gelişmekte olan pazarları hedeflediği için bu makalede 2G'ye odaklanacağız. WebPageTest testini çalıştırdıktan sonra, en üstte bir film şeridinin yanı sıra bir şelale (DevTools'dakine benzer şekilde) görüntülenir. Film şeridi, uygulamanız yüklenirken kullanıcının ne gördüğünü gösterir. 2G'de, PROXX'in optimize edilmemiş sürümünün yükleme deneyimi oldukça kötü:

Film şeridi videosunda, PROXX emüle edilmiş bir 2G bağlantısı üzerinden gerçek, düşük kaliteli bir cihaza yüklenirken kullanıcının ne gördüğü gösterilmektedir.

3G üzerinden yüklendiğinde, kullanıcı 4 saniye boyunca hiç beyazlık olmadığında görür. 2G'de ise kullanıcılar 8 saniyeden uzun bir süre boyunca hiçbir şey görmez. Performansın neden önemli olduğunu okursanız, sabırsızlık nedeniyle potansiyel kullanıcılarımızın önemli bir kısmını artık kaybettiğimizi bilirsiniz. Herhangi bir şeyin ekranda görünmesi için kullanıcının 62 KB'lık JavaScript'in tamamını indirmesi gerekir. Bu senaryoda en kötü şey, ekranda görünen her şeyin aynı zamanda etkileşimli olmasıdır. Yoksa mümkün mü dersiniz?

PROXX'in optimize edilmemiş sürümündeki [First Anningful Paint][FMP], _technically_ [interactive][TTI] değerine sahiptir, ancak kullanıcı için yararsızdır.

Yaklaşık 62 KB gzip'd JS indirildikten ve DOM oluşturulduktan sonra kullanıcı uygulamamızı görmeye başlar. Uygulama teknik olarak etkileşimlidir. Ancak görsele baktığımızda farklı bir gerçekliğin ortaya çıktığı görülüyor. Web yazı tipleri arka planda yüklenmeye devam eder ve hazır olana kadar kullanıcı hiçbir metin göremez. Bu durum İlk Anlamlı Boyama (FMP) kapsamına gelse de kullanıcı girişlerin herhangi birinin neyle ilgili olduğunu söyleyemeyeceği için kesinlikle düzgün bir şekilde etkileşimli olarak nitelendirilmez. Uygulamanın kullanıma hazır olması 3G'de bir saniye, 2G'de ise 3 saniye sürer. Sonuç olarak uygulamanın etkileşimli hale gelmesi 3G'de 6 saniye ve 2G'de 11 saniye sürer.

Şelale analizi

Artık kullanıcının ne gördüğünü bildiğimize göre, bunun nedenini belirlememiz gerekir. Bunun için şelaleye bakıp kaynakların neden çok geç yüklendiğini analiz edebiliriz. PROXX için 2G izlememizde iki önemli tehlike işaretiyle karşılaşıyoruz:

  1. Çok sayıda, çok renkli ince çizgi vardır.
  2. JavaScript dosyaları bir zincir oluşturur. Örneğin, ikinci kaynak yalnızca ilk kaynak bittikten sonra yüklenmeye başlar. Üçüncü kaynak ise yalnızca ikinci kaynak tamamlandığında yüklenmeye başlar.
Şelale, hangi kaynakların ne zaman ve ne kadar sürede yüklendiğine dair bilgi verir.

Bağlantı sayısını azaltma

Her ince çizgi (dns, connect, ssl), yeni bir HTTP bağlantısı oluşturulmasını ifade eder. 3G'de yaklaşık 1, 2G'de ise yaklaşık 2,5 saniye sürdüğünden yeni bağlantı kurmak pahalıdır. Şelalemizde aşağıdakiler için yeni bir bağlantı olduğunu görüyoruz:

  • İstek #1: index.html
  • İstek 5: fonts.googleapis.com tarafından sunulan yazı tipi stilleri
  • İstek 8: Google Analytics
  • İstek 9: fonts.gstatic.com tarafından gönderilen bir yazı tipi dosyası
  • İstek 14: Web uygulaması manifesti

index.html için yeni bağlantı kaçınılmazdır. Tarayıcının, içerikleri almak için sunucumuzla bağlantı oluşturması gerekir. Minimal Analytics gibi bir yöntemi satır içine alarak Google Analytics'in bu yeni bağlantısını önleyebilirsiniz. Ancak Google Analytics, uygulamamızın oluşturulmasını veya etkileşimli hale gelmesini engellemediğinden, uygulamanın ne kadar hızlı yüklendiği bizim için önemli değildir. İdeal olarak, Google Analytics'in boşta kaldığında, yani diğer her şey zaten yüklenmişken yüklenmesi gerekir. Böylece, ilk yükleme sırasında bant genişliği veya işlem gücü kullanmaz. Manifest'in kimlik bilgisi olmayan bir bağlantı üzerinden yüklenmesi gerektiğinden web uygulaması manifestinin yeni bağlantısı getirme spesifikasyonu tarafından belirlenir. Yine web uygulaması manifest dosyası, uygulamamızın oluşturulmasını veya etkileşimli hale gelmesini engellemez. Bu nedenle, çok fazla önem vermemiz gerekmez.

Ancak bu iki yazı tipi ve stilleri, oluşturmayı ve etkileşimi engelledikleri için sorun teşkil eder. fonts.googleapis.com tarafından sunulan CSS'ye baktığımızda, her yazı tipi için bir tane olmak üzere yalnızca iki @font-face kuralı olduğunu görürüz. Yazı tipi stilleri o kadar küçüktür ki, gereksiz bir bağlantıyı kaldırarak onu HTML'mizde satır içine almaya karar verdik. Yazı tipi dosyalarının bağlantı kurulumu maliyetinden kaçınmak için bunları kendi sunucumuza kopyalayabiliriz.

Yükleri paralel yapma

Şelaleye baktığımızda, ilk JavaScript dosyasının yüklenmesi tamamlandığında yeni dosyaların hemen yüklenmeye başladığını görebiliriz. Bu, modül bağımlılıkları için tipik bir durumdur. Ana modülümüzde muhtemelen statik içe aktarmalar olduğundan bu içe aktarmalar yüklenene kadar JavaScript çalışamaz. Burada unutulmaması gereken önemli nokta, bu tür bağımlılıkların derleme sırasında bilinmesidir. HTML'mizi aldığımız anda tüm bağımlılıkların yüklenmeye başladığından emin olmak için <link rel="preload"> etiketlerinden yararlanabiliriz.

Sonuçlar

Yaptığımız değişikliklerin neler başardığına bir bakalım. Test kurulumumuzda, sonuçları çarpıtabilecek başka değişkenleri değiştirmemek önemlidir. Bu nedenle, bu makalenin geri kalanında WebPageTest'in basit kurulumunu kullanacak ve film şeridine bakacağız:

Yaptığımız değişikliklerin neler başardığını görmek için WebPageTest'in film şeridini kullanıyoruz.

Bu değişiklikler TTI değerimizi 11'den 8,5'e düşürdü. Bu, kaldırmayı hedeflediğimiz bağlantı kurulumu süresinin yaklaşık 2,5 saniyesini oluşturuyordu. Tebrikler.

Önceden oluşturma

TTI'mızı kısa süre önce indirmiş olsak da kullanıcının 8, 5 saniye boyunca yaşamak zorunda olduğu sonsuz uzun beyaz ekrandan gerçekten etkilenmedik. Muhtemelen FMP için en büyük iyileştirmeler index.html öğelerinize stil uygulanmış işaretleme göndererek elde edilebilir. Bunu başarmak için yaygın olarak kullanılan teknikler, birbiriyle yakından ilişkili olan ve Web'de Oluşturma bölümünde açıklanan önceden oluşturma ve sunucu tarafı oluşturmadır. Her iki teknik de web uygulamasını Düğüm'de çalıştırır ve elde edilen DOM'yi HTML'de seriler. Sunucu tarafında oluşturma, bu işlemi sunucu tarafında istek bazında gerçekleştirir. Önceden oluşturma ise bunu derleme zamanında yapar ve çıkışı yeni index.html öğeniz olarak depolar. PROXX bir JAMStack uygulaması olduğundan ve sunucu tarafı olmadığından önceden işlemeyi uygulamaya karar verdik.

Önceden oluşturucuyu uygulamanın birçok yolu vardır. PROXX'te, Chrome'u herhangi bir kullanıcı arayüzü olmadan başlatan ve Node API ile bu örneği uzaktan kontrol etmenizi sağlayan Puppeteer'ı kullanmayı tercih ettik. Bunu, işaretlememizi ve JavaScript'imizi yerleştirmek ve ardından DOM'yi bir HTML dizesi olarak geri okumak için kullanırız. CSS Modülleri kullandığımızdan, ihtiyacımız olan stilleri ücretsiz olarak satır içine alan CSS elde ederiz.

  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);

Bu kullanıma sunulduğunda, FMP için bir gelişme olmasını bekleyebiliriz. Öncekiyle aynı miktarda JavaScript'i yükleyip yürütmemiz gerekecektir. Bu yüzden, TTI'nın çok fazla değişmesini beklememelisiniz. Herhangi bir gelişme olursa index.html büyüdü ve TTI'mızı biraz zorlayabilir. Öğrenmenin tek bir yolu vardır: WebPageTest'i çalıştırmak.

Film şeridi, FMP metriğimizde net bir iyileşme olduğunu gösteriyor. TTI büyük ölçüde etkilenmez.

İlk Anlamlı Boya uygulamamız 8,5 saniyeden 4,9 saniyeye, büyük bir iyileştirmeyle güncellendi. TTI'mız hâlâ 8,5 saniye civarında gerçekleştiğinden bu değişiklikten büyük ölçüde etkilenmemiştir. Burada algısal bir değişiklik gerçekleştirdik. Bazıları buna el kurşunu bile diyebilir. Oyunun ara görselini oluşturarak algılanan yükleme performansını daha iyi hale getiriyoruz.

Satır içi

Hem DevTools hem WebPageTest'in bize sağladığı başka bir metrik de İlk Bayt Süresi (TTFB) değeridir. Bu, isteğin ilk baytından alınan yanıtın ilk baytına kadar geçen süredir. Bu süre genellikle Gidiş Dönüş Süresi (RTT) olarak da adlandırılır, ancak teknik olarak bu iki sayı arasında bir fark vardır: RTT, isteğin sunucu tarafındaki işleme süresini içermez. DevTools ve WebPageTest, TTFB'yi istek/yanıt bloğunda açık bir renkle görselleştirir.

İsteğin ışıklı bölümü, isteğin yanıtın ilk baytını almayı beklediğini gösterir.

Şelalemize baktığımızda, tüm isteklerin büyük bir kısmının zamanlarının büyük bir kısmının yanıtın ilk baytının gelmesini bekleyerek geçirdiğini görebiliriz.

HTTP/2 Push aslında bu sorun için tasarlanmıştır. Uygulama geliştirici, belirli kaynakların gerekli olduğunu bilir ve bu kaynakları başka işlere ittirebilir. İstemci, ek kaynakları getirmesi gerektiğini fark ettiğinde zaten tarayıcının önbelleklerinde yer alır. HTTP/2 Push'un doğru şekilde kullanılması çok zor olduğundan önerilmez. Bu sorun alanı, HTTP/3'ün standartlaştırılması sırasında yeniden değerlendirilecektir. Şu an için en kolay çözüm, önbelleğe alma verimliliğine rağmen tüm kritik kaynakları satır içi olarak kullanmaktır.

Kritik CSS'miz, CSS Modülleri ve Puppeteer tabanlı önceden oluşturucumuz sayesinde zaten satır içine yerleştirilmiştir. JavaScript için kritik modüllerimizi ve bağımlılıklarını satır içi olarak eklememiz gerekir. Bu görevin zorluğu, kullandığınız paketleyiciye bağlı olarak değişiklik gösterir.

JavaScript'imizi satır içine alarak TTI'mızı 8,5 saniyeden 7,2 saniyeye indirdik.

Bu işlem TTI'mızdan 1 saniye tasarruf sağladı. index.html dokümanımızın, ilk oluşturma ve etkileşimli hale gelme için gerekli olan her şeyi içerdiği noktaya ulaştık. HTML, indirme işlemi devam ederken görüntülenerek FMP'mizi oluşturur. HTML'nin ayrıştırılması ve çalıştırılması tamamlandığı anda uygulama etkileşimli hale gelir.

Agresif kod bölme

Evet. index.html, etkileşimli hale gelmek için gerekli olan her şeyi içerir. Ancak daha yakından incelediğinizde geri kalan her şeyin de içinde bulunduğu ortaya çıkıyor. index.html dosya boyutu yaklaşık 43 KB. Bunu, kullanıcının başlangıçta etkileşimde bulunabileceği öğelerle ilişkili olarak söyleyelim: Oyunu yapılandırmak için bir formumuz var. Bu formda birkaç bileşen, bir başlat düğmesi ve muhtemelen kullanıcı ayarlarını kullanmaya devam etmenizi sağlayacak bazı kodlar yer alıyor. Oldukça bu kadar. 43 KB çok fazla bir boyut gibi görünüyor.

PROXX açılış sayfası. Burada yalnızca kritik bileşenler kullanılır.

Paket boyutumuzun nereden geldiğini anlamak üzere paketin neleri içerdiğini incelemek için kaynak haritası gezgini veya benzer bir araç kullanabiliriz. Tahmin edildiği gibi paketimizde oyun mantığı, oluşturma motoru, kazanma ekranı, kaybetme ekranı ve bir dizi yardımcı program yer alıyor. Açılış sayfası için bu modüllerin yalnızca küçük bir alt kümesi gerekir. Etkileşim için kesinlikle gerekli olmayan her şeyi geç yüklenen bir modüle taşımak, TTI'yı önemli ölçüde azaltır.

PROXX "index.html" içeriğinin analiz edilmesi, birçok gereksiz kaynak göstermektedir. Kritik kaynaklar vurgulanır.

Yapmamız gereken kod bölme. Kod bölme işlemi, monolitik paketinizi daha küçük parçalara ayırır. Bunlar, isteğe bağlı olarak geç yüklenebilir. Webpack, Rollup ve Parcel gibi popüler paketleyiciler, dinamik import() kullanarak kod bölmeyi destekler. Paketleyici, kodunuzu analiz eder ve statik olarak içe aktarılan tüm modülleri satır içi olarak belirler. Dinamik olarak içe aktardığınız her şey kendi dosyasına yerleştirilir ve yalnızca import() çağrısı yürütüldükten sonra ağdan getirilir. Ağa ulaşmanın elbette bir maliyeti vardır ve yalnızca zamanınız olduğunda yapılmalıdır. Burada amaç, yükleme zamanında kritik olarak ihtiyaç duyulan modülleri statik olarak içe aktarmak ve diğer her şeyi dinamik olarak yüklemektir. Ancak, kesinlikle kullanılacak modülleri geç yüklemek için en son ana kadar beklememelisiniz. Phil Walton'un Idle Until Urgent oyunu, geç yükleme ile istekli yükleme arasında sağlıklı bir orta yol oluşturmak için mükemmel bir kalıp.

PROXX içinde, ihtiyaç duymadığımız her şeyi statik olarak içe aktaran bir lazy.js dosyası oluşturduk. Böylece, ana dosyamıza lazy.js öğesini dinamik olarak içe aktarabiliriz. Ancak, bazı Preact bileşenlerimiz lazy.js içinde sonuçlandı. Bu nedenle Preact, yavaş yüklenen bileşenleri kutudan çıkardıklarında kullanamadığı için bu durum biraz karmaşıktı. Bu nedenle, gerçek bileşen yüklenene kadar bir yer tutucu oluşturmamıza olanak tanıyan küçük bir deferred bileşen sarmalayıcı yazdık.

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();
    }
  };
}

Bunu sağladığınızda, render() işlevlerimizde bir bileşenin Promise'ini kullanabiliriz. Örneğin, animasyonlu arka plan resmini oluşturan <Nebula> bileşeni, bileşen yüklenirken boş bir <div> ile değiştirilir. Bileşen yüklenip kullanıma hazır olduğunda <div>, gerçek bileşenle değiştirilir.

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

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

Tüm bunları yaptıktan sonra, index.html boyutumuzu yalnızca 20 KB'a indirdik. Bu boyut, orijinal boyutunun yarısından daha azdır. Bunun FMP ve TTI üzerinde nasıl bir etkisi olur? WebPageTest bunu gösterir!

Film şeridi doğru: TTI'mız artık 5,4 sn. İlk 11'lerimize göre büyük bir gelişme.

Yalnızca satır içi JavaScript'in ayrıştırılması ve çalıştırılmasıyla ilgili olduğundan FMP ile TTI'mız arasında yalnızca 100 ms fark vardır. 2G'de 5, 4 saniyeden sonra uygulama tamamen etkileşimli hale gelir. Daha az önemli olan diğer tüm modüller arka planda yüklenir.

Daha Fazla El Algısı

Yukarıdaki kritik modüller listemize bakarsanız, oluşturma motorunun kritik modüller arasında yer almadığını göreceksiniz. Elbette oyunun oluşturulması için oluşturma motorumuz olmadan oyun başlatılamaz. Oluşturma motorumuz oyunu başlatmaya hazır olana kadar "Başlat" düğmesini devre dışı bırakabiliriz, ancak tecrübelerimize göre kullanıcının oyun ayarlarını yapılandırması için yeterince uzun zaman geçirmesi buna gerek yoktur. Çoğu zaman, oluşturma motorunun ve kalan diğer modüllerin yüklenmesi, kullanıcı "Başlat"a bastığında tamamlanmış olur. Kullanıcının ağ bağlantısından daha hızlı olması nadir durumlarda, kalan modüllerin tamamlanmasını bekleyen basit bir yükleme ekranı gösteririz.

Sonuç

Ölçüm önemlidir. Gerçek olmayan problemlere zaman harcamamak için, her zaman optimizasyonları uygulamadan önce ölçüm yapmanızı öneririz. Ayrıca ölçümler, 3G bağlantısı olan gerçek cihazlarda veya elinizde gerçek bir cihaz yoksa WebPageTest'de yapılmalıdır.

Film şeridi, uygulamanızı yüklemenin kullanıcı için nasıl hissettiği hakkında fikir verebilir. Şelale, potansiyel olarak uzun yükleme sürelerinden hangi kaynakların sorumlu olduğunu gösterebilir. Aşağıda, yükleme performansını iyileştirmek için yapabileceklerinizin yer aldığı bir yapılacaklar listesi verilmiştir:

  • Tek bir bağlantı üzerinden mümkün olduğunca çok sayıda öğe yayınlayın.
  • Önceden yükleyin, hatta ilk oluşturma ve etkileşim için gereken satır içi kaynakları bile yükleyin.
  • Algılanan yükleme performansını iyileştirmek için uygulamanızı önceden oluşturun.
  • Etkileşim için gereken kod miktarını azaltmak üzere yoğun kod bölme özelliğinden yararlanın.

Yüksek kısıtlamaya tabi cihazlarda çalışma zamanı performansının nasıl optimize edileceğini ele aldığımız 2. bölümü kaçırmayın.