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

PROXX'te kod bölme, kod satır içi ekleme ve sunucu tarafı oluşturma özelliklerini nasıl kullandık?

Google I/O 2019'da Mariko, Jake ve ben web için modern bir Mayın Tarlası klonu olan PROXX'i kullanıma sunduk. PROXX'i diğerlerinden ayıran özelliklerden biri erişilebilirliğe odaklanmasıdır (oyunu ekran okuyucu ile oynayabilirsiniz). Ayrıca, hem özellikli telefonlarda hem de üst düzey masaüstü cihazlarda sorunsuz bir şekilde çalışır. Özellikli telefonlar birçok açıdan kısıtlıdır:

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

Ancak modern bir tarayıcı kullanıyorlar ve çok uygun fiyatlı. Bu nedenle, özellikli telefonlar gelişmekte olan pazarlarda yeniden popülerlik kazanıyor. Fiyatları, daha önce karşılayamayan yeni bir kitlenin internete girip modern web'den yararlanmasına olanak tanır. 2019'da yalnızca Hindistan'da yaklaşık 400 milyon özellikli telefon satılacağı tahmin ediliyor. Bu nedenle, özellikli telefon kullanıcıları kitlenizin önemli bir bölümünü oluşturabilir. Buna ek olarak, yükselen pazarlarda 2G'ye benzer bağlantı hızları normaldir. PROXX'i, özellikli telefon koşullarında iyi şekilde çalıştırmayı nasıl başardık?

PROXX oyun deneyimi.

Hem yükleme performansı hem de çalışma zamanı performansı dahil olmak üzere performans önemlidir. İyi performansın, kullanıcı elde tutma oranının artması, dönüşümlerin iyileşmesi ve en önemlisi kapsayıcılığın artmasıyla ilişkili olduğu gösterilmiştir. Jeremy Wagner, performansın neden önemli olduğu hakkında çok daha fazla veri ve analize sahiptir.

Bu, iki bölümden oluşan bir serinin 1. bölümüdür. 1. bölümde yükleme performansına, 2. bölümde ise çalışma zamanı performansına odaklanacağız.

Mevcut durumu yakalama

Yükleme performansınızı gerçek bir cihazda test etmek çok önemlidir. Gerçek bir cihazınız yoksa WebPageTest'i, özellikle de "basit" kurulumu kullanmanızı öneririz. WPT, taklit edilmiş 3G bağlantısı olan gerçek bir cihazda bir dizi yükleme testi çalıştırır.

3G, ölçmek için iyi bir hızdır. 4G, LTE veya yakında 5G'ye alışmış olsanız da mobil internetin gerçekliği oldukça farklı. Tren, konferans, konser veya uçakta olabilirsiniz. Bu durumda, büyük olasılıkla 3G'ye yakın bir hız elde edersiniz. Bazen bu hız daha da düşük olabilir.

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

Film şeridi videosunda, PROXX gerçek ve düşük kaliteli bir cihazda emülasyonlu 2G bağlantısı üzerinden yüklenirken kullanıcının gördüğü görüntüler gösterilmektedir.

3G üzerinden yüklendiğinde kullanıcı 4 saniye boyunca beyaz bir ekran görür. 2G'de kullanıcı 8 saniye boyunca hiçbir şey görmez. Performansın neden önemli olduğunu okumuşsanız sabırsızlık nedeniyle potansiyel kullanıcılarımızın önemli bir kısmını kaybettiğimizi bilirsiniz. Ekranda bir şeyin görünmesi için kullanıcının 62 KB'lık JavaScript'in tamamını indirmesi gerekir. Bu senaryoda olumlu bir nokta, ekranda görünen her şeyin etkileşimli olmasıdır. Yoksa mümkün mü dersiniz?

PROXX'in optimize edilmemiş sürümündeki [ilk anlamlı boyama][FMP], _teknik olarak_ [etkileşimli][TTI] olsa da kullanıcı için işe yaramaz.

Yaklaşık 62 KB sıkıştırılmış JS indirildikten ve DOM oluşturulduktan sonra kullanıcı uygulamamızı görür. Uygulama teknik olarak etkileşimlidir. Ancak görsele bakıldığında farklı bir gerçeklik ortaya çıkıyor. Web yazı tipleri arka planda yüklenmeye devam eder ve hazır olana kadar kullanıcı metin göremez. Bu durum ilk anlamlı boyama (FMP) olarak kabul edilse de kullanıcı girişlerin hiçbirinin neyle ilgili olduğunu anlayamadığından kesinlikle düzgün bir şekilde etkileşimli olarak kabul edilemez. Uygulamanın kullanıma hazır hale gelmesi için 3G'de bir saniye, 2G'de ise 3 saniye daha sürer. Aslında uygulamanın etkileşimli hale gelmesi için 3G'de 6 saniye, 2G'de ise 11 saniye sürer.

Şelale analizi

Kullanıcının ne gördüğünü bildiğimize göre neden gördüğünü anlamamız gerekiyor. Bunun için şelaya bakabilir ve kaynakların neden çok geç yüklendiğini analiz edebiliriz. PROXX için 2G izlememizde iki önemli uyarı görüyoruz:

  1. Birden fazla çok renkli ince çizgi vardır.
  2. JavaScript dosyaları bir zincir oluşturur. Örneğin, ikinci kaynak yalnızca ilk kaynak tamamlandıktan sonra yüklenmeye başlar ve üçüncü kaynak yalnızca ikinci kaynak tamamlandığında başlar.
Şelale, hangi kaynakların ne zaman yüklendiği ve bu işlemin ne kadar sürdüğü hakkında bilgi verir.

Bağlantı sayısını azaltma

Her ince çizgi (dns, connect, ssl), yeni bir HTTP bağlantısının oluşturulduğunu gösterir. Yeni bir bağlantı kurmak, 3G'de yaklaşık 1 saniye, 2G'de yaklaşık 2,5 saniye sürdüğü için maliyetlidir. Şelalemizde aşağıdakiler için yeni bir bağlantı görüyoruz:

  • İstek #1: index.html
  • İstek #5: fonts.googleapis.com'teki yazı tipi stilleri
  • 8. İstek: Google Analytics
  • 9. İstek: fonts.gstatic.com kaynağından bir yazı tipi dosyası
  • İstek #14: Web uygulaması manifesti

index.html için yeni bağlantı kaçınılmaz. Tarayıcının, içerikleri almak için sunucumuzla bağlantı oluşturması gerekir. Google Analytics için yeni bağlantı, Minimal Analytics gibi bir şey dahil edilerek önlenebilir ancak Google Analytics, uygulamamızın oluşturulmasını veya etkileşimli hale gelmesini engellemediğinden, ne kadar hızlı yüklendiğini pek önemsemiyoruz. İdeal olarak Google Analytics, diğer her şey yüklendikten sonra boş zamanınızda yüklenmelidir. Bu sayede ilk yükleme sırasında bant genişliği veya işlem gücü kullanılmaz. Web uygulaması manifesti için yeni bağlantı, kimlik bilgisi olmayan bir bağlantı üzerinden yüklenmesi gerektiğinden getirme spesifikasyonu tarafından zorunlu kılınmıştır. Yine de web uygulaması manifesti, uygulamamızın oluşturulmasını veya etkileşimli hale gelmesini engellemediği için bu konuya çok fazla önem vermemiz gerekmiyor.

Ancak bu iki yazı tipi ve stilleri, oluşturmayı ve etkileşimi engellediği için sorun teşkil ediyor. fonts.googleapis.com tarafından sağlanan CSS'ye bakarsak 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ü ki gereksiz bir bağlantıyı kaldırarak bunları HTML'mize satır içi olarak eklemeye karar verdik. Yazı tipi dosyaları için bağlantı kurulum maliyetini önlemek amacıyla bunları kendi sunucumuza kopyalayabiliriz.

Yükleri paralelleştirme

Şelaleye baktığımızda, ilk JavaScript dosyasının yüklenmesi tamamlandıktan sonra yeni dosyaların hemen yüklenmeye başladığını görebiliriz. Bu durum, modül bağımlılıkları için normaldir. Ana modülümüzde muhtemelen statik içe aktarma işlemleri vardır. Bu nedenle, bu içe aktarma işlemleri yüklenene kadar JavaScript çalıştırılamaz. Burada dikkat edilmesi gereken önemli nokta, bu tür bağımlılıkların derleme sırasında bilindiğidir. HTML'mizi aldığımız anda tüm bağımlılıkların yüklenmeye başlamasını sağlamak için <link rel="preload"> etiketlerinden yararlanabiliriz.

Sonuçlar

Değişikliklerimizin ne gibi sonuçlara yol açtığına bakalım. Test kurulumumuzda sonuçları etkileyebilecek başka değişkenleri değiştirmememiz önemlidir. Bu nedenle, bu makalenin geri kalanında WebPageTest'in basit kurulumunu kullanacağız ve film şeridine bakacağız:

Değişikliklerimizin ne gibi sonuçlar verdiğini görmek için WebPageTest'in film şeridini kullanırız.

Bu değişiklikler, TTI'mizi 11'den 8,5'e düşürdü. Bu da yaklaşık olarak kaldırmayı hedeflediğimiz 2,5 saniyelik bağlantı kurulum süresine karşılık geliyor. Tebrikler.

Önceden oluşturma

TTI'mizi azalttık ancak kullanıcının 8, 5 saniye boyunca beklemek zorunda olduğu sonsuz uzunluktaki beyaz ekranı pek etkilemedik. FMP için en büyük iyileştirmeler, index.html öğenizle stilize işaretleme göndererek elde edilebilir. Bu amaca ulaşmak için kullanılan yaygın teknikler, ön oluşturma ve sunucu tarafı oluşturmadır. Bu teknikler birbirine çok yakındır ve Web'de Oluşturma bölümünde açıklanmıştır. Her iki teknik de web uygulamasını Node'da çalıştırır ve ortaya çıkan DOM'u HTML olarak serileştirir. Sunucu tarafı oluşturma, bunu sunucu tarafında istek başına yapar. Öniz oluşturma ise bunu derleme sırasında yapar ve çıktıyı yeni index.html olarak depolar. PROXX bir JAMStack uygulaması olduğundan ve sunucu tarafı olmadığından önceden oluşturma özelliğini uygulamaya karar verdik.

Ön oluşturma aracını uygulamanın birçok yolu vardır. PROXX'te, Chrome'u herhangi bir kullanıcı arayüzü olmadan başlatan ve bu örneği bir Node API ile uzaktan kontrol etmenize olanak tanıyan Puppeteer'i kullanmayı tercih ettik. Bu yöntemi, işaretlememizi ve JavaScript'imizi eklemek ve ardından DOM'u bir HTML dizesi olarak geri okumak için kullanırız. CSS modülleri kullandığımızdan, ihtiyacımız olan stillerin CSS satır içi eklemesini ücretsiz olarak 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 sayede, FMP'mizde iyileştirmeler görebiliriz. Önceki gibi aynı miktarda JavaScript'i yükleyip yürütmemiz gerektiğinden TTI'nin çok fazla değişmesini beklemiyoruz. index.html'ümüz daha da büyüdü ve TTI'mizi biraz geciktirebilir. Bunu öğrenmenin tek yolu WebPageTest'i çalıştırmaktır.

Film şeridi, FMP metriğimizde belirgin bir iyileşme olduğunu gösteriyor. TTI bu durumdan çoğunlukla etkilenmez.

İlk Anlamlı Gösterimimiz 8,5 saniyeden 4,9 saniyeye düştü. Bu, büyük bir gelişmedir. TTI'miz hâlâ yaklaşık 8,5 saniyede gerçekleştiği için bu değişiklikten büyük ölçüde etkilenmedi. Burada yaptığımız şey algısal bir değişikliktir. Bazıları bunu el çabukluğu olarak adlandırabilir. Oyunun ara görselini oluşturarak, yükleme performansının algılanan kalitesini iyileştiriyoruz.

Satır içi yerleştirme

Hem DevTools hem de WebPageTest'in bize sunduğu bir diğer metrik ilk bayta geçiş süresi (TTFB)'dir. Bu, istek gönderildikten yanıtın ilk baytının alınması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, istek için sunucu tarafında harcanan işlem süresini içermez. DevTools ve WebPageTest, TTFB'yi istek/yanıt bloğunda açık renkle gösterir.

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

Şelalemize baktığımızda, tüm isteklerin zamanlarının çoğunu yanıtın ilk baytının gelmesini bekleyerek geçirdiğini görebiliriz.

HTTP/2 Push, başlangıçta bu sorun için tasarlanmıştı. Uygulama geliştirici, belirli kaynaklara ihtiyaç duyulduğunu bilir ve bunları iletmek için gerekli işlemleri yapabilir. İstemci ek kaynaklar getirmesi gerektiğini fark ettiğinde bu kaynaklar zaten tarayıcının önbelleğindedir. HTTP/2 Push'in doğru şekilde uygulanmasının çok zor olduğu anlaşıldığı için bu yöntemin kullanılması önerilmez. Bu sorun alanı, HTTP/3'ün standartlaştırılması sırasında tekrar ele alınacaktır. Şu anda en kolay çözüm, önbelleğe alma verimliliği pahasına tüm kritik kaynakları satır içi olarak yerleştirmektir.

Kritik CSS'miz, CSS modülleri ve Puppeteer tabanlı ön oluşturma aracımız sayesinde zaten satır içine yerleştirilmiş durumda. JavaScript için kritik modüllerimizi ve bunların bağımlılarını satır içine almamız gerekir. Bu görevin zorluğu, kullandığınız paketleyiciye bağlı olarak değişir.

JavaScript'imizi satır içine yerleştirerek TTI'mizi 8,5 saniyeden 7,2 saniyeye düşürdük.

Bu sayede TTI'mizi 1 saniye azalttık. Artık index.html'ün ilk oluşturma ve etkileşime hazır hale gelme için gereken her şeyi içerdiği bir noktaya geldik. HTML, indirme işlemi devam ederken oluşturularak FMP'mizi oluşturur. HTML ayrıştırılıp yürütüldüğünde uygulama etkileşimli hale gelir.

Agresif kod bölme

Evet, index.html'ümüz etkileşimli hale gelmek için gereken her şeyi içerir. Ancak daha ayrıntılı bir inceleme yaptığımızda, diğer her şeyi de içerdiğini görüyoruz. index.html dosyamız yaklaşık 43 KB'tır. Bunu, kullanıcının başlangıçta etkileşimde bulunabileceği öğelerle ilişkilendirelim: Oyunu yapılandırmak için birkaç bileşen, bir başlangıç düğmesi ve muhtemelen kullanıcı ayarlarını kalıcı hale getirip yükleyen bazı kodlar içeren bir formumuz var. Hepsi bu kadar. 43 KB çok fazla.

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

Paket boyutumuzun nereden geldiğini anlamak için paketin nelerden oluştuğunu öğrenmek üzere bir kaynak harita gezgini veya benzer bir araç kullanabiliriz. Tahmin edildiği gibi paketimiz oyun mantığını, oluşturma motorunu, kazanan ekranını, kaybeden ekranını ve bir dizi yardımcı programı içeriyor. Açılış sayfası için bu modüllerin yalnızca küçük bir kısmına ihtiyaç vardır. Etkileşimli olmak için kesinlikle gerekli olmayan her şeyi yavaşça yüklenen bir modüle taşımak TTI'yi önemli ölçüde düşürür.

PROXX'in "index.html" dosyasının içeriği analiz edildiğinde çok sayıda gereksiz kaynak olduğu görülüyor. Kritik kaynaklar vurgulanır.

Yapmamız gereken kodu bölme işlemidir. Kod bölme, monolitik paketinizi isteğe bağlı olarak geç yüklenebilen daha küçük parçalara ayırır. Webpack, Rollup ve Parcel gibi popüler paketleyiciler dinamik import() kullanarak kod bölme işlemini destekler. Paketleyici, kodunuzu analiz eder ve statik olarak içe aktarılan tüm modülleri satır içi olarak yerleştirir. Dinamik olarak içe aktardığınız her şey kendi dosyasına yerleştirilir ve yalnızca import() çağrısı çalıştırıldığında ağdan alınır. Elbette ağda reklam yayınlamanın maliyeti vardır ve bu işlem yalnızca zamanınız varsa yapılmalıdır. Burada temel kural, yükleme sırası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üllerin gecikmeli yüklenmesini son ana kadar beklememelisiniz. Phil Walton'un Idle Until Urgent (Acil Olmadıkça Boş Dur) yaklaşımı, yavaş yükleme ve istekli yükleme arasında sağlıklı bir orta yol için mükemmel bir modeldir.

PROXX'te, ihtiyacımız olmayan her şeyi statik olarak içe aktaran bir lazy.js dosyası oluşturduk. Ardından ana dosyamızda lazy.js dosyasını dinamik olarak içe aktarabiliriz. Ancak Preact bileşenlerimizden bazıları lazy.js içinde yer aldı. Preact, hazırda yüklenmemiş bileşenleri işleyemediğinden bu durum biraz karmaşık bir hal aldı. Bu nedenle, asıl bileşen yüklenene kadar yer tutucu oluşturmamıza olanak tanıyan küçük bir deferred bileşen sarmalayıcısı 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();
    }
  };
}

Bu şekilde, 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üklendikten ve kullanıma hazır hale geldikten sonra <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 bu önlemleri uyguladıktan sonra index.html'ü 20 KB'a indirdik. Bu, orijinal boyutun yarısından az. Bunun FMP ve TTI üzerinde nasıl bir etkisi olur? WebPageTest size bu konuda yardımcı olabilir.

Film şeridi, TTI'mizin şu anda 5,4 saniye olduğunu doğruladı. Orijinal 11'lerimizden önemli ölçüde daha iyi.

Yalnızca satır içi JavaScript'in ayrıştırılması ve yürütülmesi söz konusu olduğundan FMP ve TTI'miz arasında yalnızca 100 ms fark vardır. Uygulama, 2G'de yalnızca 5,4 saniye sonra tamamen etkileşimli hale geliyor. Daha az önemli olan diğer tüm modüller arka planda yüklenir.

More Sleight of Hand

Yukarıdaki kritik modüller listemize bakarsanız oluşturma motorunun kritik modüller arasında olmadığını görürsünüz. Elbette, oyunu oluşturacak bir oluşturma motorumuz olana kadar oyun başlatılamaz. Oluşturma motorumuz oyunu başlatmaya hazır olana kadar "Başlat" düğmesini devre dışı bırakabiliriz ancak deneyimlerimize göre kullanıcının oyun ayarlarını yapılandırması genellikle o kadar uzun sürer ki bu gerekli değildir. Çoğu zaman, kullanıcı "Başlat"a bastığı zaman oluşturma motoru ve diğer modüller yüklenir. Kullanıcının ağ bağlantısından daha hızlı olması gibi nadir durumlarda, kalan modüllerin tamamlanmasını bekleyen basit bir yükleme ekranı gösteririz.

Sonuç

Ölçüm yapmak önemlidir. Gerçek olmayan sorunlarla uğraşmak için zaman kaybetmemek amacıyla, optimizasyonları uygulamadan önce her zaman ölçüm yapmanızı öneririz. Ayrıca, ölçümler 3G bağlantısı olan gerçek cihazlarda veya gerçek cihaz yoksa WebPageTest'te yapılmalıdır.

Film şeridi, uygulamanızın yüklenmesi sırasında kullanıcının ne hissettiğine dair fikir verebilir. Şelale, olası uzun yükleme sürelerinden hangi kaynakların sorumlu olduğunu size söyleyebilir. Aşağıda, yükleme performansını iyileştirmek için yapabileceğiniz işlemlerin listesi verilmiştir:

  • Tek bir bağlantı üzerinden mümkün olduğunca çok öğe yayınlayın.
  • İlk oluşturma ve etkileşim için gereken kaynakları önceden yükleyin veya hatta satır içi olarak ekleyin.
  • Algılanan yükleme performansını iyileştirmek için uygulamanızı önceden oluşturun.
  • Etkileşimli içerikler için gereken kod miktarını azaltmak amacıyla agresif kod bölme özelliğinden yararlanın.

Son derece kısıtlanmış cihazlarda çalışma zamanı performansını nasıl optimize edeceğimizi ele alacağımız 2. bölüm için takipte kalın.