Ö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ığımız.

Google I/O 2019'da Mariko, Jake ve ben, web için modern bir Minesweeper klonu olan PROXX'i gönderdik. PROXX'i farklı kılan asıl nokta, erişilebilirliğe (ekran okuyucuyla oynatabilirsiniz!) ve özellikli telefonlarda, ileri teknoloji masaüstü cihazlarda olduğu gibi çalışabilme yeteneğidir. Gelişmiş özellikli telefonlar çeşitli şekillerde kısıtlanır:

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

Ancak modern bir tarayıcı çalıştırırlar ve oldukça uygun fiyatları vardır. Bu nedenle de öne çıkan telefonlar, gelişmekte olan pazarlarda yeniden popülerleşiyor. Piyasadaki fiyat noktaları, daha önce bunu karşılayamayan yepyeni bir kitlenin internete girip modern web'den yararlanmasına olanak tanıyor. 2019 yılında yalnızca Hindistan'da yaklaşık 400 milyon özellikli telefonun satılacağı tahmin ediliyor. Bu nedenle, özellikli telefon kullanıcıları kitlenizin önemli bir bölümünü oluşturuyor olabilir. Ayrıca, gelişmekte olan pazarlarda 2G'ye benzer bağlantı hızları standarttır. PROXX'in özellikli telefon koşullarında iyi çalışmasını nasıl başardık?

PROXX oynanabilirliği.

Performans önemlidir ve buna hem yükleme performansı hem de çalışma zamanı performansı dahildir. İyi performansın kullanıcıları elde tutma oranının artması, dönüşümlerin artması ve en önemlisi kapsayıcılığın artmasıyla ilişkili olduğu gözlemlenmiştir. Jeremy Wagner, performansın neden önemli olduğu hakkında çok daha fazla veriye ve bilgiye sahip.

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 tespit etme

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" kurulum başlıklı makaleye bakın. WPT, emülasyonlu 3G bağlantısı olan gerçek bir cihazda yükleme testleri uygular.

3G, ölçüm için iyi bir hızdır. 4G, LTE veya yakında 5G'ye alışkın olsanız da mobil internet gerçeği epey farklı görünüyor. Belki trende, konferansta, konserde ya da uçaktasınızdır. Bu deneyimde yaşayacağınız deneyim büyük olasılıkla 3G'ye daha yakın, hatta bazen daha kötüsüdür.

Bununla birlikte, PROXX hedef kitlesinde açık olarak özellikli telefonları ve gelişmekte olan pazarları hedeflediğinden bu makalede 2G'ye odaklanacağız. WebPageTest, testini çalıştırdıktan sonra bir şelale (Geliştirici Araçları'nda gördüğünüze benzer) ve üstte 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ıya 4 saniye süreyle beyaz hiçlik gösterilmez. 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 okursanız sabırsızlık nedeniyle şu anda potansiyel kullanıcılarımızın önemli bir kısmını kaybettiğimizi bilirsiniz. Bir şeyin ekranda 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 [First Meaningful Paint][FMP] _Technically_ [etkileşimli][TTI] ancak kullanıcı için bir faydası yoktur.

gzip'teki JS yaklaşık 62 KB indirildikten ve DOM oluşturulduktan sonra kullanıcı uygulamamızı görebilir. 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 ediyor ve hazır olana kadar kullanıcı metin göremez. Bu durum İlk Anlamlı Boyama (FMP) olarak kabul edilse de kullanıcı girişlerin neyle ilgili olduğunu anlayamayacağından kesinlikle etkileşimli olarak nitelendirilmez. Uygulamanın hazır olması için 3G üzerinde bir saniye, 2G ağında ise 3 saniye daha geçmesi gerekir. Özetle, uygulamanın etkileşimli hale gelmesi 3G'de 6 saniye, 2G'de 11 saniye sürer.

Şelale analizi

Artık kullanıcının ne gördüğünü bildiğimize göre nedenini öğrenmemiz gerekir. Bunun için şelaleye bakabilir ve kaynakların neden çok geç yüklendiğini analiz edebiliriz. PROXX için 2G izleme sistemimizde iki önemli uyarı işareti görüyoruz:

  1. Çok renkli, birden çok ince çizgi vardır.
  2. JavaScript dosyaları bir zincir oluşturur. Örneğin, ikinci kaynak yalnızca ilk kaynak tamamlandığında yüklenmeye başlar. Üçüncü kaynak ise yalnızca ikinci kaynak tamamlandığında yüklenmeye başlar.
ziyaret edin.
Ş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ı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:

  • 1. İstek: index.html
  • 5. istek: fonts.googleapis.com adlı satıcının yazı tipi stilleri
  • 8. İstek: Google Analytics
  • 9. istek: fonts.gstatic.com adlı satıcıdan yazı tipi dosyası
  • 14. İstek: 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. Minimal Analytics gibi bir öğeyi satır içine alarak yeni Google Analytics bağlantısının önüne geçilebilir ancak Google Analytics, uygulamamızın oluşturulmasını veya etkileşimli hale gelmesini engellemediğinden ne kadar hızlı yüklendiğini önemsiyoruz. İdeal olarak, Google Analytics boştayken, diğer her şey zaten yüklenmişken 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. Tekrar belirtmek gerekirse web uygulaması manifesti, uygulamamızın oluşturulmasını veya etkileşimli hale gelmesini engellemediğinden çok fazla önem verilmesine gerek yok.

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

Yüklemeleri paralel yapma

Şelaleye baktığımızda, ilk JavaScript dosyasının yüklenmesi bitince yeni dosyaların hemen yüklenmeye başladığını görüyoruz. Bu, modül bağımlılıkları için normal bir durumdur. Ana modülümüzde muhtemelen statik içe aktarmalar vardır, bu nedenle bu içe aktarmalar yüklenene kadar JavaScript çalışamaz. 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şladığından emin olmak 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ı çarpıtabilecek diğer değişkenleri değiştirmemek çok önemlidir. Bu nedenle, bu makalenin geri kalanında WebPageTest'in basit kurulumunu kullanacağız ve film şeridine bakacağız:

Değişikliklerimizin neler başardığını görmek için WebPageTest'in film şeridini kullanırız.

Bu değişiklikler, TTI'mızı 11'den 8,5'e düşürdü. Bu, kaldırmayı amaçladığımız yaklaşık 2,5 sn'lik bağlantı kurulumu süresini ifade ediyor. Bizi tebrik ederiz.

Önceden işleme

Kısa süre önce TTI'mızı azaltmış olsak da kullanıcının 8, 5 saniye boyunca dayanması gereken sonsuz uzun beyaz ekranı etkilemedik. Muhtemelen FMP için en büyük iyileştirmelere index.html dosyanıza stillendirilmiş işaretleme göndererek elde edebilirsiniz. Bunu başarmak için yaygın olarak kullanılan teknikler, yakından ilişkili olan ve Web'de Oluşturma bölümünde açıklanan önceden işleme ve sunucu tarafı oluşturmadır. Her iki teknik de web uygulamasını Node'da çalıştırır ve sonuç olarak elde edilen DOM'yi HTML'ye serileştirir. Sunucu tarafı oluşturma, bu işlemi sunucu tarafında istek başına yapar. Önceden işleme ise bu işlemi derleme zamanında yapar ve çıktıyı yeni index.html olarak depolar. PROXX bir JAMStack uygulaması olduğundan ve sunucu tarafı olmadığından önceden işlemeyi uygulamaya karar verdik.

Önceden oluşturucu uygulamanın birçok yolu vardır. PROXX'te Puppeteer'ı kullanmayı tercih ettik. Bu hizmet, 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üllerini kullandığımızdan, ihtiyaç duyduğumuz stillerin CSS satır içiini ücretsiz olarak alıyoruz.

  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'mizin iyileştirilmesini bekleyebiliriz. 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 bir yolu vardır: WebPageTest'i çalıştırma.

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

İlk Anlamlı Boyama uygulamamız 8,5 saniyeden 4,9 saniyeye yükseldi. Bu da büyük bir iyileştirmedir. TTI'mız hâlâ 8,5 saniye civarında olduğundan bu değişiklikten pek etkilenmemiştir. Burada, algısal bir değişiklik gerçekleştirdik. Hatta kimileri ona el becerisi de diyebilir. Oyunun ara görselini oluşturarak algılanan yükleme performansını daha iyi hale getiriyoruz.

Satır içine alma

Hem Geliştirici Araçları hem de WebPageTest'in bize sağladığı bir diğer metrik, İlk Bayt Zamanı'dır (TTFB). Bu, isteğin gönderilen ilk baytından alınan yanıtın ilk baytı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. DevTools ve WebPageTest, TTFB'yi istek/yanıt bloğu içinde açık bir renkle görselleştirir.

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ğunluğunu yanıtın ilk baytının gelmesini beklerken geçirdiğini görebiliyoruz.

Bu sorun, HTTP/2 Push'un ilk başta tasarlandığı yönüyle ortaya çıktı. Uygulama geliştirici, belirli kaynakların gerekli olduğunu bilir ve bu kaynakları devreye aldırabilir. İstemci ek kaynaklar getirmesi gerektiğini fark ettiğinde, bunlar zaten tarayıcının önbelleklerindedir. HTTP/2 Push'un doğru yapılması çok zor olduğu için önerilmez. Bu sorun alanı, HTTP/3'ün standartlaştırılması sırasında yeniden ele alınacaktır. Şimdilik en kolay çözüm, önbelleğe alma verimliliği pahasına, tüm kritik kaynakları satır içi yapmaktır.

Kritik CSS'miz, CSS Modülleri ve Puppeteer tabanlı önceden oluşturucumuz sayesinde zaten entegre edildi. JavaScript için kritik modüllerimizi ve bağımlılıklarını satır içi yapmamız gerekir. Bu görevin zorluğu, kullandığınız paketleyiciye bağlı olarak farklılık gösterir.

ziyaret edin.
JavaScript'imiz sayesinde TTI sistemimizi 8,5 saniyeden 7,2 saniyeye düşürdük.

Bu işlem, TTI'mızı 1 saniye kısalttı. Artık index.html sayfamızın, ilk oluşturma ve etkileşimli hale gelme için gereken her şeyi içerdiği noktaya ulaştık. HTML, indirilirken FMP'mizi oluşturarak oluşturulabilir. HTML, ayrıştırma ve yürütme işlemini tamamladığı anda uygulama etkileşimli hale gelir.

Agresif kod bölme

Evet. index.html, etkileşimli olmak için gereken her şeyi içerir. Ancak daha yakından incelediğimizde diğer her şeyin de bulunduğunu fark ettik. index.html boyutu yaklaşık 43 KB'tır. Bunu, kullanıcının başlangıçta neyle etkileşimde bulunabileceğiyle ilişkilendirelim: Birkaç bileşen, bir başlatma düğmesi ve muhtemelen kullanıcı ayarlarını yükleyecek bir kod içeren bir oyunu yapılandırdığımız bir formumuz var. Hepsi bu kadar. 43 KB oldukça büyük görünüyor.

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

Paket boyutumuzun nereden geldiğini anlamak için bir kaynak harita gezgini veya benzer bir araç kullanarak paketin içeriğini analiz edebiliriz. Sizin de tahmin edildiği gibi paketimizde oyun mantığı, oluşturma motoru, kazanma ekranı, kaybedilen ekran ve pek çok yardımcı program var. Açılış sayfası için bu modüllerin yalnızca küçük bir alt grubuna ihtiyaç vardır. Etkileşim için kesinlikle gerekli olmayan her şeyi geç yüklenen bir modüle taşımak, TTI'yi önemli ölçüde azaltır.

PROXX "index.html" dosyasının içeriği analiz edildiğinde, birçok gereksiz kaynak ortaya çıkıyor. Kritik kaynaklar vurgulanıyor.

Bunun için kod bölme işlemini gerçekleştirmemiz gerekir. Kod bölme, monolitik paketinizi isteğe bağlı 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 yapar. 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üğünde ağdan getirilir. Elbette, ağa ulaşmanın bir maliyeti vardır ve yalnızca vaktiniz olduğunda yapılmalıdır. Burada amaç, kritik olarak gerekli olan modülleri yükleme zamanında statik olarak içe aktarmak ve diğer her şeyi dinamik olarak yüklemektir. Ancak kesinlikle kullanmanız gerekecek modülleri geç yüklemek için son ana kadar beklememelisiniz. Phil Walton'un Idle Until Urgent videosu, geç yükleme ile istekli yükleme arasında sağlıklı bir orta yol için mükemmel bir model.

PROXX'te, ihtiyacımız olmayan her şeyi statik olarak içe aktaran bir lazy.js dosyası oluşturduk. Böylece ana dosyamızda lazy.js öğesini dinamik olarak içe aktarabiliriz. Ancak Preact bileşenlerimizden bazıları lazy.js içinde kaldı. Bu durum, Preact'in geç yüklenen bileşenleri kutudan çıkaramadığı için biraz komplikasyon olarak ortaya çı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 yaptıktan sonra, render() işlevlerimizde bir bileşenin Promise özelliğ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ı uyguladıktan sonra index.html dosyamızı yalnızca 20 KB'a, yani orijinal boyutun yarısından daha azına indirdik. Bunun VYP ve TTI üzerinde ne gibi bir etkisi olur? WebPageTest

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.

FMP ile TTI arasında yalnızca 100 ms. vardır, çünkü bu işlem sadece satır içi JavaScript'in ayrıştırılması ve çalıştırılmasıyla ilgilidir. 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.

Daha Hızlı El becerisi

Yukarıdaki kritik modül listemize bakarsanız oluşturma motorunun kritik modüllerin bir parçası olmadığını görürsünüz. Elbette, oluşturmak için oluşturma motorumuz olana kadar oyun başlatılamaz. "Başlat" düğmesini devre dışı bırakabiliriz. düğmesini tıklayın. Ancak tecrübelerimize göre kullanıcı, oyun ayarlarını yapılandırmaya yetecek kadar uzun sürüyor ve bu gerekli değil. Çoğunlukla, kullanıcı "Başlat"a basana kadar oluşturma motorunun ve kalan diğer modüllerin yüklenmesi biter. Kullanıcının ağ bağlantısından daha hızlı olduğu nadir durumlarda, geri 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ı üzerinden gerçek cihazlarda veya elinizde gerçek bir 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ürelerine yol açtığını belirtebilir. Yükleme performansını iyileştirmek için yapabileceklerinizin bir listesini aşağıda bulabilirsiniz:

  • Bir bağlantı üzerinden mümkün olduğunca çok sayıda öğe iletin.
  • İlk oluşturma ve etkileşim için gerekli olan kaynakları önceden yükleyin, hatta 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 agresif kod bölme yöntemlerinden yararlanın.

Son derece kısıtlamalı cihazlarda çalışma zamanı performansını nasıl optimize edeceğinizi ele aldığımız 2. bölümü kaçırmayın.