Ö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. Piyasadaki fiyat noktaları, daha önce buna ayıramayacakları yepyeni bir kitlenin internete girip 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ı kitlenizin önemli bir bölümünü oluşturabilir. Ayrıca, gelişmekte olan pazarlarda 2G'ye benzer bağlantı hızları standarttır. PROXX'i, özellikli telefon koşullarında iyi şekilde çalıştırmayı nasıl başardık?

PROXX oynanabilirliği.

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 dizinin 1. bölümüdür. 1. bölüm yükleme performansına odaklanır, 2. bölüm ise çalışma zamanı performansına odaklanır.

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, kullanıcıların uygulamanız yüklenirken gördüklerini 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ülasyonlu bir 2G bağlantı üzerinden gerçek, düşük teknoloji bir cihaza yüklenirken kullanıcının gördüğü görüntüler gösteriliyor.

3G üzerinden yüklendiğinde kullanıcı 4 saniye boyunca beyaz bir ekran görür. 2G bağlantısı üzerinden kullanıcı, 8 saniyeden uzun bir süre 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 senaryonun iyi tarafı, ekranda görünen ikinci öğenin de 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 baktığımızda farklı bir gerçekliği görüyoruz. 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 hazır olması için 3G üzerinde bir saniye, 2G bağlantıda ise 3 saniye daha geçmesi gerekir. Sonuç olarak uygulamanın etkileşimli hale gelmesi 3G'de 6 saniye, 2G'de 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 var.
  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şturulmasını ifade eder. Yeni bir bağlantı kurmak maliyetli bir işlemdir, çünkü 3G'de yaklaşık 1 sn, 2G'de yaklaşık 2,5 sn.sürer. Şelalemizde şunlar için yeni bir bağlantı var:

  • İstek #1: index.html
  • 5. İstek: 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 bir 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 şekilde, ilk yükleme sırasında bant genişliği veya işlem gücü kullanmaz. Manifestin kimlik bilgisi olmayan bir bağlantı üzerinden yüklenmesi gerektiğinden, web uygulaması manifestinin yeni bağlantısı getirme spesifikasyonunda açıklanmış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 iki yazı tipi ve stilleri, oluşturmayı ve etkileşimi engelledikleri için sorun teşkil eder. 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

Yaptığımız değişikliklerin neler olduğuna birlikte göz atalı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 işe yaradığını görmek için WebPageTest'in film şeridini kullanıyoruz.

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, bu işlemi sunucu tarafında istek başına yapar. Öniz oluşturma ise bu işlemi derleme sırasında yapar ve çıktıyı yeni index.html öğeniz 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 Puppeteer'ı kullanmayı tercih ettik. Bu uygulama, Chrome'u herhangi bir kullanıcı arayüzü olmadan başlatır ve size söz konusu örneği Node API ile uzaktan kontrol etmenizi sağlar. Bunu, işaretlememizi ve JavaScript'imizi yerleştirmek için kullanırız ve ardından DOM'yi bir HTML dizesi olarak yeniden okuruz. CSS modülleri kullandığımızdan, ihtiyacımız olan stillerin CSS'sini ücretsiz olarak satır içi olarak ekleyebiliriz.

  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. Yine de öncekiyle aynı miktarda JavaScript yükleyip yürütmemiz gerekiyor. Bu nedenle, TTI'nın çok fazla değişmesini beklemiyoruz. Herhangi bir şey olursa index.html daha büyük hale geldi ve TTI'mızı biraz geri çevirebilir. Bunu öğrenmenin tek yolu WebPageTest&#39;i çalıştırmaktır.

Film şeridi, FMP metriğimizde belirgin bir iyileşme olduğunu gösteriyor. TTI bundan pek etkilenmez.

İlk Anlamlı Gösterimimiz 8,5 saniyeden 4,9 saniyeye düştü. Bu, önemli bir gelişmedir. TTI'mız hâlâ 8,5 saniye civarında olduğundan bu değişiklikten pek etkilenmemiştir. 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ılanmasını daha iyi hale getiriyoruz.

Satır içine alma

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üreye genellikle Gidiş Dönüş Süresi (RTT) da denir ancak teknik olarak bu iki sayı arasında bir fark vardır: RTT, sunucu tarafındaki isteğin işlenme süresini içermez. Geliştirici Araçları ve WebPageTest, TTFB'yi istek/yanıt bloğunda açık renkle gösterir.

Bir isteğin ışık bölümü, isteğin yanıtın ilk baytını almak için 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 anladıktan sonra bunlar zaten tarayıcının önbelleklerinde yer alır. 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 yeniden 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çi. 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, indirilirken FMP'mizi oluşturarak oluşturulabilir. 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ı kaydedip yükleyen bazı kodlar içeren bir formumuz var. Hepsi bu kadar. 43 KB çok fazla.

PROXX 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 "index.html" dosyasının içeriğini analiz etmek, birçok gereksiz kaynak gösteriyor. 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() özelliğini 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 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ğa ulaşmanın bir maliyeti vardır ve yalnızca vaktiniz olduğunda yapılmalıdır. Burada amaç, 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, 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();
    }
  };
}

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 bunu doğruluyor: TTI'mız şu anda 5,4 sn. İlk 11 oyunlarımıza göre büyük bir gelişme.

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. 2G'de sadece 5, 4 saniye sonra uygulama tamamen etkileşimli hale geliyor. Daha az gerekli 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. Tabii ki oyunu oluşturmak için gereken 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 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ı yüklemenin kullanıcılar için nasıl hissettiği hakkında bilgi verebilir. Şelale, hangi kaynakların uzun yükleme sürelerinden kaynaklandığını belirleyebilir. Yükleme performansını iyileştirmek için yapabileceğiniz işlemlerin yapılacaklar listesi aşağıda verilmiştir:

  • Bir bağlantı üzerinden mümkün olduğunca çok sayıda öğe iletin.
  • İ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.