- MongoDB ve PostgreSQL için bir veritabanı GUI’si geliştirirken BSON ve JSONB türlerini, iç içe sütunları, arama, düzenleme, sabitleme ve sürüklemeyi desteklemek gerekiyordu; bunun için yaklaşık 1 yıl boyunca iki eksenli sanallaştırma ve durum yönetimi yapısı optimize edildi
- Kaynak belgelerden ayrı olarak, görüntü dizelerini, türleri, düzleştirilmiş yolları, sütun sırasını ve arama sonuçlarını önceden hesaplayan bir gölge tablo oluşturuldu; ekranda görünen satır ve sütunlar ise sabit boyutlu DOM ile render edildi
- Kaydırma yolunda pasif event listener,
requestAnimationFrame, buffer ve hysteresis ile hız takibi uygulandı; layout özellikleri yerinetransformveopacitykullanılarak ana thread işi azaltıldı - Hücre başına ikonlar paylaşılan SVG arka plan görsellerine dönüştürüldü, editörler yalnızca gerektiğinde mount edildi ve satır ile sütunları konuma göre izleyen DOM pooling ile kaydırma sırasında node oluşturma ortadan kaldırıldı
- Canvas, DOM’a göre 60fps için daha yüksek bir performans tavanı sunuyor; ancak metin, seçim, erişilebilirlik ve özellik genişletme açısından dezavantajlı olduğundan, gerçek metin seçimini ve hızlı geliştirmeyi koruyan DOM tabanlı tasarım tercih edildi
Hedefler ve ilk kısıtlar
- Başlangıçta basit bir 2 boyutlu dizi ve iç içe döngülerle yola çıkıldı, ancak süreç yaklaşık 1 yıllık aralıklı optimizasyon çalışmasına dönüştü
- Veritabanı GUI’sindeki tablo yalnızca görüntüleme amaçlı değil, çok çeşitli durumları ve etkileşimleri de desteklemek zorundaydı
- MongoDB’nin tüm BSON türlerini ve PostgreSQL gibi sistemlerdeki JSONB’yi anlayıp türe göre renkli ikonlar göstermeliydi
- Sorgu sonucunu değiştiren
"123"dizisi ile123tamsayısı gibi türler ayırt edilmeliydi - İç içe belgeler gerçek alt sütunlara genişletilmeli, iç içe yolun tamamında arama yapılıp hücre içindeki eşleşen kısımlar vurgulanmalıydı
- Sütun yeniden sıralama, yeniden boyutlandırma, sabitleme, hücre içinde düzenleme ve değer, satır, sütunları görsel sorgu oluşturucuya sürükleme gibi özellikler gerekliydi
- Bu özelliklerin durumu kaynak belgelerde yoktu ve kaydırmadan sonra da korunmalıydı; bu yüzden ayrı bir render yapısına ihtiyaç vardı
1. aşama: Her şeyi doğrudan render etmek
- Satırları ve alanları iç içe dolaşıp tüm hücreleri oluşturan yaklaşım 100 satırda çalışsa da büyük veri kümelerinde çöküyordu
- 10.000 satır × 30 sütun yaklaşık 300.000 DOM node’u üretir ve framework’ün change detection mekanizması bunların üzerinden tekrar tekrar geçer
- Bir DOM node’unun tarayıcı iç yapılarıyla birlikte yaklaşık 1KB tükettiği varsayılırsa, asıl veriden önce yüzlerce MB gerekir
- 60fps için kare bütçesi 16,7ms’dir ve style, layout, paint de bu süreyi paylaşır
- Gerçek uygulamada yaklaşık 20 sütunlu 1.000 satırı render etmeye çalışırken başarısız olundu
- Yalnızca bir kısmını render etmek için o anda görünen satır ve sütunları, sıralarını, iç içe alan genişletmelerini ve arama sonuçlarını ayrı olarak izlemek gerekiyordu
2. aşama: Görüntü durumunu gölge tabloyla ayırmak
- Kaynak belgeler iç içeydi ve türleri tutarsızdı; bu yüzden doğrudan render girdisi olarak kullanmak zordu
- MongoDB’de ObjectId, Decimal128, timestamp ve binary gibi BSON değerleri bulunur
- SQL verilerinde JSONB ve zaman dilimi içeren timestamp’ler vardır; bu nedenle her hücre için formatın belirlenmesi gerekir
- Render döngüsü içinde formatı belirlemek, aynı maliyetin her karede tekrar etmesine yol açar; ayrıca kaynak veride sütun sırası, genişletme durumu ve arama sonucu gibi tablo durumları da yoktur
- Gölge tablo, kaynak belgeleri değiştirmeden gerçek tablonun göstereceği durum için referans noktası görevi görür
- Yükleme sırasında bir kez kurulur, durum değiştiğinde güncellenir ve kaydırma sırasında değiştirilmez
- Her hücre için kırpılmış görüntü dizisi, kesinleşmiş tür ve düzleştirilmiş yol önceden hesaplanır
- Görüntü dizisi sınırlandırılarak 16MB’lık bir belgenin render durumunda doğrudan 16MB’lık diziye dönüşmesi engellenir
- Tür, ikonları, editörü ve arama biçimini belirler
"address.geo.lat"gibi düzleştirilmiş yollar anahtar olarak kullanılır; böylece her seferinde ağaç dolaşımı yapılmaz
- İç içe nesneler genişletildiğinde alt yollar gerçek sütunlara yükseltilir; sıralama, arama sonuçları, sütun sırası ve genişletme durumu da aynı yapıda tutulur
- Bu aşamada DOM sayısı azalmaz, ancak sonraki adımlarda sütun sırası ve genişliğini hızlı hesaplamak için temel hazırlanır
3. aşama: Dikey sanallaştırma ve hayalet kaydırma alanı
- Dikey sanallaştırma, yalnızca viewport içindeki satırları ve küçük bir buffer’ı render eder; kalan yükseklik ise sahte bir alan olarak bırakılır
- Hayalet alan (phantom), yüksekliği
rowCount × rowHeightolan iç konteynerdir- 1 milyon satır × 40px, neredeyse içeriği olmayan 40 milyon px yüksekliğinde bir
divüretir - Tarayıcı bu yüksekliği temel alarak yerel kaydırma çubuğunu ve kaydırma davranışını sağlar
- 1 milyon satır × 40px, neredeyse içeriği olmayan 40 milyon px yüksekliğinde bir
- Görüntü aralığı şu hesapla bulunur
firstRow = floor(scrollTop / rowHeight)lastRow = floor((scrollTop + viewportHeight) / rowHeight)- Çok küçük hareketlerde yeniden render etmemek için iki tarafa da buffer satırlar eklenir
- Görünen satırlar,
firstRow × rowHeightkonumundaki slab konteynerine yerleştirilir; buradatopyerinetransformkullanmak daha avantajlıdır - Kullanıcı 1 milyon satır görür, ama DOM’da yaklaşık 40 satır bulunur
- Ancak 300 sütun varsa, yalnızca 40 satır bile 12.000 hücre üretir; bu yüzden sütunların da sanallaştırılması gerekir
4. aşama: Değişken genişlikleri işleyen yatay sanallaştırma
- Belge veritabanı koleksiyonlarında yüzlerce alan oluşabildiği için sütun sanallaştırması da gerekir
- Sütun genişlikleri sabit olmadığından, sabit değerli bölme yerine kümülatif toplam ve ikili arama kullanılır
position[n] = width[0] + ... + width[n-1]biçimindeki kümülatif toplam ile her sütunun x koordinatı tek bir dizi erişimiyle elde edilir- Kaydırma offset’i
x’e karşılık gelen sütun, kümülatif toplam dizisinde ikili aramayla bulunur - Sütun sayısı 1.000 olsa bile arama mikrosaniyeler içinde biter
- Kümülatif toplam, yalnızca sütun yeniden boyutlandırma, gizleme veya yeniden sıralama gibi genişliğin gerçekten değiştiği durumlarda yeniden oluşturulur; kaydırma sırasında oluşturulmaz
- Sol ve sağda yaklaşık 200px buffer bırakılarak sonraki sütunlar ekranda görünmeden önce önceden render edilir
- İki eksenli sanallaştırmadan sonra render alanı veri boyutundan bağımsız biçimde yaklaşık 40 satır × 12 sütunda sabit kalır
- 500 sütunlu koleksiyonlarda bile aynı anda yalnızca yaklaşık 12 sütun bulunduğundan yükleme süresi artmadı
- Render edilecek alan küçüldü, ancak saniyede yüzlerce kez oluşan scroll event’lerinde aralığı yeniden hesaplama sorunu kaldı
5. aşama: Kaydırma yolunda kare bütçesini yönetmek
- Scroll işlemleri, style, layout ve paint ile 16,7ms kare bütçesini paylaştığı için mümkün olan en az işi yapmalıdır
- Pasif event listener’lar framework change detection’ının dışında kaydedildi
- Tarayıcıya
preventDefaultçağrılmayacağı bildirilir; böylece compositor, JavaScript’i beklemeden pikselleri hareket ettirebilir - Scroll event’inin kendisi framework’ün render kontrolünü tetiklemez
- Tarayıcıya
- Birden çok event, kare başına tek çalışmaya indirgenir
- Yalnızca en güncel scroll konumu kaydedilir ve tek bir
requestAnimationFramecallback’i planlanır - Aynı karede oluşan 12 event bile tek bir aralık hesabıyla işlenir
- Yalnızca en güncel scroll konumu kaydedilir ve tek bir
- Handsontable kaynak kodundan alınan hızlı çizim çıkışı uygulandı
- Yeni görüntü aralığı mevcut render buffer’ının içindeyse, yalnızca iki tamsayı karşılaştırması yapılıp hemen geri dönülür
- Buffer sınırlarına hysteresis uygulandı
- Görüntü aralığı buffer kenarına yaklaşık 40px kadar yaklaştığında yeniden kurulum yapılır
- Yaklaşık 200px’lik buffer yeni konumun merkezine yeniden yerleştirilerek boş kenarların görünmesi ya da sınırda tekrar tekrar kurulum yapılması önlenir
- Event’ler arasındaki px/ms değerini izleyen bir hız dedektörü eklendi
- Uygulamada 10px/ms üzeri hızlı flick olarak kabul edilip render durduruldu
- Yerel kaydırmanın hayalet alan üzerinde hareket etmesine izin verildi; hız dengelenince slab yeniden dolduruldu
- JavaScript işi azaltıldıktan sonra bile kare düşüşleri devam etti; style, zebra desenleri ve ikonları da içeren layout ile paint sonraki darboğaz haline geldi
6. aşama: Layout özelliklerinin maliyetini kaldırmak
- Tarayıcının ana thread’i style, layout ve paint’ten sorumludur; compositor ise önceden çizilmiş katmanları GPU üzerinde hareket ettirir
- Animasyonu compositor üzerinde işleyebilen özellikler
transformveopacity’dir;top,left,width,height,background-colorgibi özellikler ana thread’i uyandırır - Satır numarası ve sabit sütun panelinin scroll senkronizasyonunda
topgüncellendiğinde saniyede 60 kez zorunlu layout oluştu- Bu, aynı görünümü koruyup ana thread maliyetini ortadan kaldıracak şekilde
translate3dile değiştirildi
- Bu, aynı görünümü koruyup ana thread maliyetini ortadan kaldıracak şekilde
- Satır bazlı arka planlarla yapılan zebra desenleri, gövdenin tamamına uygulanan tek bir
repeating-linear-gradientile değiştirildi- Satır yüksekliği CSS değişkeniyle aktarıldı
- Tarayıcı yalnızca iki satırlık tek bir tile’ı rasterize eder ve bunu GPU texture’ı üzerinden tekrar tekrar kopyalar
- Satır bazlı class binding ortadan kalkar; ayrıca gövde ile sabit panel aynı tile’ı paylaştığından renk uyumsuzluğu da önlenir
- Sütun ayırıcı çizgileri, 40 milyon px yüksekliğindeki hayalet alanın tamamına değil yalnızca o anda render edilen slab yüksekliğine çizildi
- Normal kaydırma akıcı hale geldi; ancak yeni hücreler oluşturan pencere geçişlerinde hücre başına gereksiz DOM birikimi nedeniyle maliyet devam etti
7. aşama: İkonları ve editörleri hafifletmek
- Veritabanı gridindeki tür ikonları dekoratif değil; ObjectId, string, integer ve JSONB gibi değerlerin anlamını ayırt etmeye yarayan işlevsel öğelerdir
- İlk uygulama, her hücreye font icon elementi ekliyordu; ancak ek DOM node’ları ve gliflerin metin render yolu büyük maliyet yarattı
- İkonlar hücrenin kendi
background-imageözelliğine taşındı ve SVG data URI olarak kodlandı- Aynı türdeki tüm hücreler aynı URI dizisini referans alır
- Tarayıcı her tür için ikonu yalnızca bir kez rasterize eder ve tekrar tekrar önbelleğe alınmış GPU texture’ından kopyalar
- Ayrı DOM node’u olmadan tekrarlayan dekorasyon gösterilebilir
- Hücre düzenlemede hibrit render yaklaşımı uygulandı
- Normal durumda hücre yalnızca düz metin ve bir
spankullanır - Ancak çift tıklandığında, ağır tür farkındalıklı editör bileşeni portal benzeri biçimde o hücrenin üzerine mount edilir
- Normal durumda hücre yalnızca düz metin ve bir
- Tüm hücrelerde framework bileşeni kullanılırsa instance oluşturma maliyeti birikir; ayrıca grid kütüphanesini hücre renderer bileşenleriyle birleştirmek performansı düşürebilir
- Hücrelerin kendisi hafifledi, fakat yeni bir sütun göründüğünde framework’ün içeriği aynı kalan diğer hücreleri de yeniden oluşturması sorunu sürdü
8. aşama: Konum tabanlı DOM yeniden kullanımı
- Angular’daki
trackBy, React ve Vue’dakikeygibi izleme ölçütleri, pencere kaydığında mevcut elementlerin yeniden kullanılıp kullanılmayacağını ya da yok edilip yeniden oluşturulacağını belirler - Satırlar yeni veriye yeniden bağlanmayıp her seferinde tamamen değiştirilirse, bileşenler, DOM node’ları ve event listener’lar sürekli yeniden oluşturulmak zorunda kalır
- Satırlar ve sütunlar veri değerine göre değil, ekrandaki konuma göre izlendi
- Aynı yaklaşık 40 satır elementi ve sütun elementi korunur, yalnızca içerikleri yeni değerlerle değiştirilir
- Grid, bir object pool gibi çalışır ve kaydırma sırasında yeni DOM ayırmaz
- Bu aşamada iki eksenli kaydırma ve pencere yeniden kurulum maliyeti çözüldü; ancak sütun bırakma anındaki tek karelik titreme ya da satır numaralarında yarım piksel kayma gibi son rötuş sorunları kaldı
9. aşama: İnce etkileşimleri tamamlamak
- Sütun sürükleme sırasında mevcut sütunlar
transformile yeni sıradaymış gibi taşınır; bırakıldığında ise yeniden sıralama iletransformsıfırlaması aynı render geçişinde yapılır- Son sürükleme karesi ile ilk yeniden sıralama karesi piksel düzeyinde aynı hale getirilerek geçiş görünmez kılınır
- Tooltip’ler her hücreye bağlanmaz; bunun yerine konteyner üzerinde tek bir delegated hover listener ile yönetilir
- Yalnızca imlecin altındaki hücre için tooltip hesaplanır
- SQL’deki JSONB değerini okunur biçimde yazdırmak gibi pahalı dizi üretimleri de gerçek hover anına ertelenir
- Satır numarası sütunundaki 1px kenarlık, veri satırlarıyla metin taban çizgisini yarım piksel kaydırarak scroll sırasında titreşime yol açıyordu
- Bu, iki sütunun da aynı box model kullanması sağlanarak düzeltildi
- Zebra desenleri, yeniden kullanılan DOM konumuna göre değil mutlak satır indeksine göre belirlendi; böylece sanallaştırma penceresi değişse bile renkler yanıp sönmez
- Kendi grid’ini geliştirmenin gerekçesi olan özellikler de korundu
- İç içe belgeler JSON dizisi olarak bırakılmayıp gerçek alt sütunlara genişletildi
- İç içe yol eşleşmeleri hücre içinde vurgulandı
- Tür bilgisi taşıyan değerler doğrudan grid’den görsel sorgu oluşturucuya sürüklendi
- Tipik bir grid kütüphanesine bu özellikleri eklemenin maliyeti, renderer’ı doğrudan sahiplenmenin maliyetinden daha yüksekti
10. aşama: Mevcut yüksek performanslı grid’lerin kaynak kodunu okumak
- Frontend performansı öğrenirken diğer projelerin kaynak kodunu okumak en yüksek değeri sağlayan alışkanlıklardan biri oldu; belgelenmemiş optimizasyonlar herkese açık depolarda duruyordu
- İncelenen DOM grid’i AG-Grid’de şu teknikler kullanılıyordu
- Açık kare başına zaman bütçesi ve öncelikli iş kuyruğuyla DOM işleri zaman dilimlerine ayrılıyordu
- Sütun viewport’u hash’lenerek değişmeyen scroll tek bir string karşılaştırmasıyla geçiliyordu
- Satırlar scroll yönüne doğru oluşturuluyor, böylece kullanıcının ilerlediği taraftaki içerik önce gösteriliyordu
- Yeni hücreler önce oluşturuluyor, mevcut hücrelerin yok edilmesi sonraya bırakılıyor; böylece yeni içerik kaybolan içerikten önce çiziliyordu
- Hücre başına event listener’ların ölçülmüş bir darboğaz olduğu görülmüş, bu yüzden konteyner düzeyinde event delegation kullanılmıştı
- Canvas tabanlı grid’ler DOM’u atlayarak her karede yüzlerce metni layout ve style yeniden hesaplaması olmadan yeniden çizer
- Aşırı etkileşim altında bile 60fps korunabildiğinden, performans tavanı DOM grid’lerden daha yüksektir
- Piksel ızgarası dışında rasterize edilen metin bulanık görünebilir
- Seçim yalnızca hücre düzeyinde çalışır; ayrıca ellipsis de elle çizilmedikçe görünmez
- Her yeni hücre özelliği için hem çizim kodu hem de hit-testing kodu eklemek gerekir
- Sonuçta DOM tercih edilerek keskin metin, gerçek metin seçimi, erişilebilirlik ve hızlı özellik geliştirme korunmuş oldu
- Canvas düzeyinde akıcılığa ulaşılamasa da, gerekli kullanıcı deneyimi ve geliştirme maliyeti gözetilerek yapılan bilinçli bir ödünleşimdir
Henüz yorum yok.