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?
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ü:
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?
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:
- Birden fazla çok renkli ince çizgi var.
- 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.
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:
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'i çalıştırmaktır.
İ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.
Ş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.
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.
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.
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.
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.