3 puan yazan GN⁺ 3 시간 전 | Henüz yorum yok. | WhatsApp'ta paylaş
  • 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 yerine transform ve opacity kullanı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 ile 123 tamsayı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 × rowHeight olan 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
  • 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 × rowHeight konumundaki slab konteynerine yerleştirilir; burada top yerine transform kullanmak 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
  • Birden çok event, kare başına tek çalışmaya indirgenir
    • Yalnızca en güncel scroll konumu kaydedilir ve tek bir requestAnimationFrame callback’i planlanır
    • Aynı karede oluşan 12 event bile tek bir aralık hesabıyla işlenir
  • 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 transform ve opacity’dir; top, left, width, height, background-color gibi özellikler ana thread’i uyandırır
  • Satır numarası ve sabit sütun panelinin scroll senkronizasyonunda top güncellendiğinde saniyede 60 kez zorunlu layout oluştu
    • Bu, aynı görünümü koruyup ana thread maliyetini ortadan kaldıracak şekilde translate3d ile değiştirildi
  • Satır bazlı arka planlarla yapılan zebra desenleri, gövdenin tamamına uygulanan tek bir repeating-linear-gradient ile 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 span kullanı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
  • 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’daki key gibi 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 transform ile yeni sıradaymış gibi taşınır; bırakıldığında ise yeniden sıralama ile transform sı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.

Henüz yorum yok.