Hepimiz modern web geliştirmedeki kutsal kuralı duymuşuzdur, kural asla çiğnenmemelidir."Ana iş parçacığını asla engellemeyin ."
Bir web geliştiricisi olarak bunu neredeyse kaçıramazsınız; neredeyse her performans kılavuzunda yer alır ve adil olmak gerekirse, bu iyi bir tavsiyedir. Hepimiz tarayıcının ana iş parçacığınıntek dişli, yani aynı anda yalnızca bir şey yapabilir.
Ayrıca, bildiğimiz gibi,ana konu sadece bizim değil; tarayıcının oluşturma motoru, girdi işleyicileri ve diğer kritik görevlerle paylaşıyoruz. Sonuç olarak, ana ileti dizisine ne kadar az zaman ayırırsak, bir uygulama o kadar hızlı yanıt verir. Bu, kullanıcı arayüzü ile herhangi bir hesaplama arasında sert bir çizgi olması gerektiğine ve bu çizginin aşılmaması gerektiğine kendimizi ikna ettiğimiz için arka plan çalışanlarıyla görevleri paylaşmamıza yol açıyor.
Ve “önerilen” bir mimari böyle görünür.
Ama şunu söyleyebilirim kibazenverileri bir çalışana taşımak, ana iş parçacığının işi yapmasına izin vermekten daha yavaştır.
Bunu birkaç ay önce Fastary adlı ekran görüntüsü alma özelliklerine sahip bir Chrome uzantısı oluştururken öğrendim. Bir ilaç kullandıktan sonra bile, tüm testlerimde yaklaşık 2 ila 3 saniyelik bir gecikme bulup durdum.Ekran Dışı Belge(Chrome uzantılarında bir arka plan işlemi) tuval işlemlerini yürütmek için. Ne de olsa bir ekran görüntüsü görevi gecikme olmadan anında hissettirmelidir.
Refleks olarak, kullanıcı arayüzünü dondurmaktan kaçınmak için işi ana iş parçacığından uzaklaştırmamız oldukça ironiktir, ancak bazen bu işi taşıma eylemi (örneğin, serileştirme, kopyalama ve serileştirme) de kullanıcı arayüzünü dondurabilir. VeBazenarka planın işi yapmasına izin vermenin önerilen yaklaşımı, işi sadece ana iş parçacığı üzerinde yapmaktan daha yavaş olabilir.
Biraz ondan bahsedelim
Tarayıcı Bağlam İzolasyonunun Mimarisiİşleri bir perspektife oturtmak için, tarayıcı bağlamlarını neden izole ettiğimizi ve birbirleriyle nasıl iletişim kurduklarını anlayalım.iletişimPart.
Tarayıcı tek bir ortamdan daha fazlasıdır. Farklı ortamlar aynı anda çalışır, her birinin kendi bellek alanı, nelere erişebileceği ve kuralları vardır:
- . ana dişen aşina olduğumuz şeydir; JavaScript mantığının çalıştığı, Dom'un yaşadığı, stillerin oluşturulduğu ve kullanıcıların etkileşime girdiği yer burasıdır.
- . Web ÇalışanlarıjavaScript'i DOM erişimi olmadan da çalıştırabilen ayrı iş parçacıklarıdır. Bunu çoğunlukla ağır veri görevleri için kullanıyoruz.
- . Servis Çalışanlarıağ isteklerini engellemekten sorumlu ağla ilgili proxy'lerdir ve sayfa kapatıldığında bile çalışabilir.
- Ve sonraKrom uzantı bağlamları, burada arka plan hizmet çalışanlarımız, içerik komut dosyalarımız ve Ekran Dışı Belgelerimiz var (bu makale için ilgili olanlar).
Bunların her biri diğerlerinden izole edilmiştir. Bir web çalışanı veya arka plan komut dosyası, ana diziden farklı bir bellek alanında yaşar. Birbirlerinin değişkenlerine veya mantığına ulaşıp okuyamazlar ve bu"hiçbir şey paylaşmayan" mimari.
Bu izole ortamlar nasıl iletişim kurar? API'leri kullanarak birbirlerine açıkça mesaj gönderirler, örneğinpostMessage().
Yapılandırılmış Klon Algoritması
postMessage()tarayıcıya bir veri parçası almasını ve bunu talep eden bağlama teslim etmesini söyler. Ancak bunu yapmak için tarayıcı,Yapılandırılmış Klon Algoritması (SCA).
Muhtemelen aşinasınızdırJSON.stringify(). SCA benzer, ancak çok daha güçlü ve daha akıllıdır. En basit haliyle SCA, derin, özyinelemeli bir kopyalama işlemidir, yani klonlama. Verildiği tüm veri yapısı boyunca yürür, her bir değeri klonlar, taşınabilir bir formatta seri hale getirir, bu baytları hedef bağlamlara gönderir ve ardından alıcı taraftaki orijinal nesneyi yeniden oluşturur.
SCA hızlıdır veya belki de hızlıdır.ish... gibi küçük bir normal yapılandırma nesnesi için{theme: "dark "}, fark edilemez; fark etmiyorsun bile. Bununla birlikte, SCA senkronize bir engelleme olduğu için ağır verilerle uğraşırken hikaye değişirO Nyani maliyet, verilerinizin boyutuyla doğrusal olarak artar.
Bunu bir perspektife koyalım. Bir kullanıcı bir düğmeye tıklar ve dahili olarak, işlenmek üzere bir arka plan çalışanına 8 MB'lık bir görüntü yükü gönderilir. AradığınızdapostMessage(), ana iş parçacığı bu serileştirme ve kopyalama işlemini çalıştırmak için yaptığı işlemi derhal durdurmalıdır.
Öyleyse, verileri paketlemek, göndermek, paketten çıkarmak ve başlangıca geri dönmek için gereken süre, yalnızca ana iş parçacığındaki verileri işlemek için geçen süreden daha uzunsa, neden bunun yerine bunu yapmıyorsunuz?Peki ya Devredilebilir Nesneler?
Eminim bazılarınız zaten düşünüyordur,"Neden sadece Aktarılabilir nesneleri kullanmıyorsunuz ?"Ve bu geçerli bir nokta. Bunun hakkında konuşalım.
Gerçekten ultra yüksek performanslı web uygulamalarını takip eden geliştiriciler genellikleDevredilebilir nesnelerÖrneğin:ArrayBufferdiye soruyorlar.ImageBitmapVeyaMessagePort) Yapılandırılmış Klon Algoritmasını atlamak için. Bunun nedeni, bir nesneyi aktarırken kopyalama yapmamanızdır (SCM gibi). Bunun yerine, tarayıcı verilerin sahipliğini bir bağlamdan diğerine geçirir.
Tarayıcı, gönderen bağlamın verilere erişimi anında kaybettiği ve alıcı bağlamın tam kontrolü ele geçirdiği bir devir gerçekleştirir. Aslında delicesine hızlıdır.Chrome Developers ’benchmark, büyük bir 32MB aktarımArrayBuffersCM ile klonlama yaparken yaklaşık 300 ms'ye kıyasla 7 ms'nin altında sürebilir. Bu 43x hız artışıdır.

Ama tüm iyi şeyler gibi, dezavantajları da var. Birkaçını saymak gerekirse:
- Gönderdikten sonra kaybedersiniz.
Kullanıcı arayüzünün hala bu verilere ihtiyacı varsa (resim önizlemesi göstermek gibi), artık ona erişemezsiniz. - Tüm veriler aktarılamaz.
Düz bir JS nesnesi değildir. Bir Blob değildir. Bir Base64 dizesi bile değildir. - API sınırlamaları.
Tarayıcı uzantıları bağlamında, Chrome'un dahili mesajlaşması (chrome.runtime.sendMessage) geleneksel olarak her şeyi JSON serileştirme yoluyla zorlar.
Bu yüzden, ekran görüntüsü uzantım söz konusu olduğunda, Aktarılabilir nesneler bir seçenek değildi.
Yine de Neden Bağlamları Yalıtıyoruz?Neden bağlamları izole etmekle uğraşıyoruz ki? Neden her şeyi ana konuya bırakmıyoruz?
Uzun süredir devam eden CPU görevlerini bir arka plan dizisine aktarmak kesinlikle yapılacak en doğru şeydir. Tarayıcının yeni bir çerçeve boyaması gerekiyorher 16,6 ms'de birşeyleri akıcı tutmak; yani>50 ms süren herhangi bir görev genellikle "uzun" olarak kabul edilir. Arka plana boşaltma kesinlikle yapılacak en doğru şeydir.
Ancak sorun şu ki,"ana iş parçacığını asla engellemeyin"sormadan mutlak bir kuralabu görevin işlenmesi mi yoksa taşınması mı pahalı?
Artık kuralın "asla ana iş parçacığını çok uzun süre engellememekten" daha az "asla ana iş parçacığını engellememek" olduğunu fark ettim.
Doğru Mimari Yanlış Mimari OlduğundaFastary uzantısıyla amacım, yerel bir uygulamanın beklediğiniz kadar sorunsuz ve anında çalışmasını sağlayarak yerel bir uygulama gibi hissettirmekti.
Bildiğiniz gibi,önerilen yaklaşımarka planda DOM çalışmasını işlemek için Ekran Dışı Belgeyi kullanmak. Ama benim için sürpriz oldu, bu farklı bir dönüş aldı.
Ekran Dışı Belge API'si açık bir kazanan. Tamamen arka planda çalışan gizli, görüntülenmeyen bir belge oluşturursunuz. Bir DOM'a sahiptir ve Canvas'ı destekler. Örneğin, bir ekran görüntüsünü kırpmak, birden fazla ekran görüntüsünü birbirine dikmek, ağır görüntü manipülasyonu yapmak veya bir filigran eklemek istersem, bunun için Ekran Dışı Belge yapıldı.
Bunun en iyi yaklaşım olmadığı ortaya çıktı. Mimarim şuydu:
- Arka plandaki Servis Çalışanı, aşağıdaki ekran görüntüsünü yakalar:
chrome.tabs.captureVisibleTab(), Base64 kodlu bir veri URL dizesi döndürür. - Hizmet İşçisinin kullandığı arka plan
chrome.runtime.sendMessage()bu görüntü yükünü Ekran Dışı Belgeye göndermek için. - Ekran Dışı Belge görüntüyü alır,
<img>öğesi, daha sonra kullanıcının kırpma koordinatlarını uygulamadan önce bir tuvale çizer, sonucu kodlar ve işlenmiş görüntüyü arka plan çalışanına geri gönderir.
Ama test ettiğimde, ekran görüntüsü anında gelmedi. Daha önce de söylediğim gibi, tutarlı bir 2 -3 saniye gecikme vardı.
Ne zaman olduğunu anladımcaptureVisibleTab()bir ekran görüntüsü alır, bir Base64 URL dizesi döndürür ve standart bir 1080p ekranda, görüntünün ne kadar ayrıntılı olduğuna bağlı olarak bu dize yaklaşık 1 MB veya daha fazla olabilir. Varsayılan olarak görüntünün boyutunu otomatik olarak iki katına çıkarma eğiliminde oldukları için modern Retina ekranlarda (örneğin, MacBook'lar) daha da ilginç hale geliyor.
Görüntü yükü iki katına çıkarılabileceğinden ve uzantı mesajlaşmasının JSON serileştirmesine (bu yazı itibariyle) dayandığından, potansiyel olarakbüyük senkron iletişimtüm bir gidiş - dönüş yolculuğuna mal olur.
Görüntü dizesi verileri en az iki kez JSON serileştirilir: bir kez Ekran Dışı Belgeye girerken ve bir kez işlenen sonuçlarla birlikte arka plan çalışanına geri dönerken. Ekran Dışı Belge içinde yapılan gerçek görüntü işleme (kırpma) şüphesiz hızlıydı, ancak aktarım ek yükü için aynı şeyi söyleyemem.
Retina Yüksek DPI Sorunu
Gecikmenin kendisi yeterli değilmiş gibi, oldukça ince bir hata fark ettim — şimdi düşünüyorum da, bu benim cehaletimin daha fazlasıydı. Ekran görüntüsü alındıktan sonra, kırpma sonucu görüntüyü garip bir şekilde ölçeklendirecek veya yanlış koordinatlarla sonuçlanacak şekilde tamamen kapalıydı.
Bir kullanıcı kırpmak için bir bölge seçtiğinde, içerik komut dosyasının kutu koordinatlarınıgetBoundingClientRect(), ölçülenCSS pikselleri; Dom bunu kullanır. Ancak ekran görüntüsü Chrome'da yerel olarak yakalandığında, tarayıcı otomatik olarak kırpmaz; bunun yerinefiziksel donanım pikselleritam ekran görüntüsünü almak için. Ve tarayıcı şunları kullanır:devicePixelRatio(DPR) bir CSS pikselini kaç fiziksel pikselin temsil etmesi gerektiğini bilmek için kullanılır. Temel olarak, Retinal ekrandaki bir kullanıcı (DPR = 2) 400x300 CSS piksellik bir alanı vurgularsa, gerçek yakalanan görüntü alanı 800x600 fiziksel pikseldir.
Not:Bir CSS pikseli, standart bir monitörde 1 fiziksel piksele (DPR 1) eşittir. Bununla birlikte, bir Mac Retina ekranda veya modern bir 4K monitörde, DPR genellikle 2 veya 3 'tür.
Doğru bir mahsul için, bu iki farklı ölçüm sistemini doğru DPR ile uygulamam, yani mahsul koordinatlarını DPR ile ölçeklendirmem gerekiyordu. Ancak unutmayın, Ekran Dışı Belgelerin fiziksel ekranı yoktur. Herhangi bir görüntünün işlenmesi, 1 'e eşit bir varsayılan DPR'ye sahip olacaktır. Bunu düzeltmek için, tam olarakdevicePixelRatioetkin sekmeden seri hale getirin, görüntü yükünün yanına geçirin ve Ekran Dışı Belge içinde ölçeklendirme matematiğini manuel olarak yapın. Karmaşıklık artmaya başlar.
Ya altın kuralı çiğnersem ve bunun yerine ana konu üzerinde çalışırsam?
Ana Konu Üzerinde ÇalışmakBazı geliştiriciler, UI görevlerinin ana dizide çalışması gereken tek şey olduğunu savunacaktır, ancak buna tam olarak katılmıyorum. Şahsen, işin inanılmaz derecede iyi olması koşuluyla, acil sonuçlara ihtiyaç duyan kullanıcı tarafından açıkça teşvik edilen eylemlerin bazen ana başlıkta sağlam bir geçiş yapabileceğine inanıyorum.hızlı(örneğin, 1s).
Yaptığım şey buydu: Ekran Dışı Belgeyi hurdaya çıkarıp mantığı yeniden tasarlamak. Bunun yerine:
Arka Plan → [serialize] → Ekran Dışı Belge → [serialize] → Arka Plan → İçeriği Komut Dosyası
… Tüm görüntü işlemeyi etkin sekmede çalıştırmaya karar verdim:
- Arka plan Servis Çalışanı ekranı yakalar ve Base64 dizesini alır (öncekiyle aynı).
- Arka plan, yükü kullanarak etkin sekmedeki içerik komut dosyasına doğrudan gönderir
chrome.scripting.executeScript(). - İçerik komut dosyası (ana iş parçacığı üzerinde çalışan) yükü alır, bir tuvale çizer, doğru DPR değerini kullanarak kırpmayı gerçekleştirir ve sonucu panoya kopyalar.
// Background Script
const screenshotUrl = await chrome.tabs.captureVisibleTab (undefined, { format: "png "});
// İşleme işlevini içerik komut dosyası olarak etkin sekmeye enjekte edin
chrome.scripting.executeScript({
target: {tabId:activeTab.id },
func: processAndCopyImage,
args: [{base64Image:screenshotUrl, cropData:userSelection}})
;
Bu yaklaşım, JSON serileştirmesinin gerektirdiği birden fazla bağlam atlamasını ve gidiş - dönüş yolculuğunu tamamen ortadan kaldırır. Tek bağlamlar arası aktarım, veri URL'sinin arka plandan içerik komut dosyasına gönderilmesini içerir.
İçerik komut dosyası doğrudan gerçek, aktif tarayıcı sekmesinin içinde çalıştığından, Retina DPI sorunu esasen kendi kendine çözüldü, çünkü monitörün gerçekdevicePixelRatio.
Ama odada fark etmiş olabileceğiniz bir fil var.
Elbette, görüntü artık ana dizide işleniyor ve arka plan aktif sekmedeki tuvali değiştiriyor. Teknik olarak ana konuyu engelliyor olabilirim. "Ana ileti dizisini engellememe" kuralını "ana ileti dizisini engellememe" olarak değiştirdiğim yer burası...almayacağım..” Bu özel durumda, en azından, kullanıcının yaklaşık bir saniye boyunca talep ettiği bir görevin ana dizisini engellemek haklı çıkarılabilir. Aynı zamanda tam tersi de çalışır: belkiveri aktarım maliyeti işlem maliyetinden büyükse süreçleri izole etmeyin.
Sonuç: Ne Zaman İzole Edilmeli Ne Zaman EdilmemeliBunu, görevin şu olup olmadığına bağlı olan zihinsel bir modele indirgedim:
1. İşlem Ağır Görevler (CPU'ya Bağlı)
Bunlar, birincil maliyetin verilerin kendisinin boyutu değil, hesaplama olduğu görevlerdir. Bunlar, görüntü sıkıştırma, ses profili oluşturma, fizik simülasyonu vb. gibi hesaplamalar veya ağır dönüşümler yapmak için zamanın çoğunun harcandığı görevlerdir.
Bu görevlerin transfer maliyeti, fiili işe kıyasla çok küçüktür.
2. Veri Ağır Görevler (Data - Bound)
Bu görevler tam tersidir. Bu görevler sadece boyutu nedeniyle pahalıdır. İşlem süresi neredeyse önemsizdir, ancak verilerin taşınması pahalıdır, örneğin görüntü kırpma, bir diziyi filtreleme, sığ kopyalama vb.
Benim özel durumumda, görevi arka plana boşaltmak çoğunluklanegatif toplam verimliliği50 ms'lik bir işlem yapmak için megabaytlarca veriyi taşımaktan bahsediyorsak, arka plana boşaltmanın bir faydası yoktur.
Belki şu şekilde düşünebiliriz:
Toplam Süre = Serileştirme Maliyeti
+ Toplu Taşıma
+ Arka Plan İşlem Süresi
+ Dizisizleştirme Maliyeti
Buna bakıldığında, eğer"arka plan işlem süresi"operasyonunuzdaki en baskın görevdir, o zaman kesin kazanan izolasyondur. Ancak serileştirme, artı serileştirmeden arındırma, artı transit bu maliyeti aşarsa, o zaman işleri izole etmeye gerek yoktur.
Ve görevin CPU ağırlıklı mı yoksa veri ağırlıklı mı olduğunu anlayamıyorsanız, bunu ölçmek kesinlikle zarar vermez, örneğin,performance.mark()VePerformans ölçümüCivarındapostMessagetransfer maliyetinin profilini çıkarmak için yapılan aramalar.




