Mengapa Pokok JSON Besar Tersekat-sekat: Tatal Maya dan Strategi Runtuh
Inti pati: Tersekat-sekat bermula apabila nod DOM dicipta, bukan apabila anda menatal. Merangkumi pemaparan runtuh dahulu, senarai maya, penyerlahan carian dan sempadan Worker, dengan metrik.

Kesimpulan utama: pecahkan "perlahan" kepada penghuraian, penciptaan nod, susun atur dan cat, serta interaksi, kemudian ukur setiap peringkat. Laksanakan dalam urutan ini — runtuhkan secara lalai, tambah senarai maya, pindahkan pengiraan ke Worker, dan hadkan penyerlahan kepada baris yang kelihatan. Melompat terus ke senarai maya tanpa meruntuhkan hanya mengecilkan set kelihatan; pengiraan semula pokok penuh masih menelan kos anda.
Tersekat-sekat biasanya datang daripada ratusan ribu nod DOM yang anda cipta untuk 5MB itu, bukan daripada 5MB itu sendiri.
1. Pecahkan "perlahan" kepada empat peringkat
Apabila pengguna berkata "ia membeku semasa dibuka," masalah boleh berada dalam empat peringkat yang sama sekali berbeza, masing-masing dengan penawar berbeza:
| Peringkat | Pencetus biasa | Ambang dapat dirasai | Kos dominan |
|---|---|---|---|
| Hurai dan bina objek | Membuka fail, menampal teks | Mana-mana tugas melebihi 50ms | Mengimbas string, membina graf objek |
| Cipta nod DOM | Memaparkan seluruh pokok sekali gus | Puluhan ribu nod | Memperuntukkan elemen, nod teks, objek gaya |
| Susun atur dan cat | Paparan pertama, menatal | Mana-mana bingkai melebihi 16.7ms | Geometri, reflow, cat semula |
| Respons interaksi | Kembang, runtuh, cari | Melebihi 100ms terasa lambat | Meratakan dan memaparkan semula |
Main thread pelayar melakukan satu perkara pada satu masa. Jika mana-mana peringkat mendudukinya lebih daripada 50ms, menatal dan menaip menggugurkan bingkai; beberapa tugas sedemikian berturut-turut dan halaman kelihatan mati. Pengoptimuman pertama bukanlah requestAnimationFrame; ia adalah mengukur peringkat mana yang sebenarnya menghabiskan masa.
2. Bilangan nod ialah musuh sebenar
Paparan pokok menggandakan bilangan nod secara reka bentuk. Satu baris JSON biasanya menjadi "inden + kunci + titik dua + nilai + koma + chevron," jadi anggaran konservatif ialah 3 hingga 6 nod DOM per baris:
| Saiz fail | Anggaran baris | Nod DOM paparan pokok (anggaran) | Rupa pemaparan sekali gus |
|---|---|---|---|
| 100 KB | 2,500 | 10,000 | Biasanya boleh diterima |
| 1 MB | 25,000 | 100,000 | Jelas tersekat-sekat, mungkin beberapa saat skrin kosong |
| 5 MB | 125,000 | 500,000 | Sangat mungkin tidak responsif |
| 20 MB | 500,000 | Melebihi 2 juta | Memori dan susun atur kedua-duanya runtuh |
Setiap elemen lebih daripada beberapa puluh bait objek: ia membawa peraturan gaya, kotak susun atur, dan arahan cat. Sebaik dicipta, nod masih menelan kerja susun atur dan gaya walaupun kemudian disembunyikan oleh induk yang runtuh, selagi ia belum benar-benar dialihkan. "Paparkan semuanya, kemudian sembunyikan" bukan pengoptimuman; ia hanya menangguhkan kos ke reflow seterusnya.
3. Runtuhkan dahulu, virtualkan kemudian
Tatal maya menyelesaikan "terlalu banyak baris kelihatan." Runtuh menyelesaikan "kita sepatutnya tidak perlu melihat sebanyak ini baris sama sekali." Urutannya penting: untuk dokumen sejuta baris yang dikembangkan secara lalai, senarai maya yang sempurna pun mesti mengira semula keseluruhan pokok apabila pengguna mengklik "runtuhkan semua."
Strategi runtuh yang boleh dilaksanakan:
- Kembangkan hanya dua tahap pertama secara lalai. Akar dan medan terdekatnya kelihatan; tahap lebih dalam menunjukkan ringkasan seperti
items: Array(12000); - Letakkan jenis dan kiraan dalam ringkasan. Pengguna yang menyemak "adakah nama medan betul" jarang memerlukan sepuluh ribu rekod;
- Cache baris per subtree. Setiap nod yang dikembangkan memiliki tatasusunan barisnya yang diratakan; runtuh membuangnya, dan kembang mengira semula hanya subtree itu;
- Jangan imbas semula seluruh pokok pada setiap togol. Jika pengembangan berlaku pada baris 3,000, ganti hanya julat itu dan biarkan baris sekeliling utuh.
/**
* Insert a subtree's rows after its parent instead of re-flattening the whole tree.
* @param rows Current visible row array
* @param parentIndex Index of the parent row
* @param childRows Flattened rows of the subtree
* @returns A new visible row array
*/
function replaceSubtreeRows(
rows: TreeRow[],
parentIndex: number,
childRows: TreeRow[],
): TreeRow[] {
const nextRows = rows.slice();
nextRows.splice(parentIndex + 1, 0, ...childRows);
return nextRows;
}
Corak yang perlu dielakkan ialah "satu kembang, satu susun atur semula penuh." Berjalan semula graf objek lengkap pada setiap klik membuat kos membesar secara linear dengan dokumen. Dengan cache per subtree, satu pengembangan hanya menelan kos sebanyak subtree itu.
4. Senarai maya: paparkan hanya viewport
Sebaik runtuh mengurangkan bilangan baris, bakinya masih boleh menjadi puluhan ribu baris. Senarai maya menetapkan ketinggian bekas kepada "jumlah baris x ketinggian baris tetap" dan mencipta elemen hanya untuk viewport serta penampan kecil di atas dan di bawah.
const ROW_HEIGHT = 24;
const OVERSCAN = 8;
/**
* Compute the row range that the viewport should render.
* @param rows All visible rows
* @param scrollTop Current scroll offset of the container
* @param viewportHeight Visible height of the container
* @returns Rows to render, the start index, and the vertical offset
*/
function getViewportRows(
rows: TreeRow[],
scrollTop: number,
viewportHeight: number,
): { visibleRows: TreeRow[]; startIndex: number; offsetTop: number } {
const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
const end = Math.min(
rows.length,
Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) + OVERSCAN,
);
const visibleNodes = rows.slice(start, end);
return {
visibleRows: visibleNodes,
startIndex: start,
offsetTop: start * ROW_HEIGHT,
};
}
Kekangan yang tidak boleh dirunding:
- Ketinggian baris mesti tetap. Hanya dengan itu
scrollTop / ROW_HEIGHTboleh memberikan indeks secara langsung. Membenarkan baris berbalut memaksa anda mengukur dan cache setiap ketinggian, yang menambah satu urutan magnitud dalam kerumitan dan geletis; - Offset dengan
transformatau kedudukan mutlak. Jangan tolak kandungan ke bawah dengan elemen spacer, atau setiap tatal mengubah susun atur; - Gunakan kunci stabil. Kunci baris mengikut laluan, bukan indeks tatasusunan, atau mengembangkan satu baris memasang semula setiap baris selepasnya;
- Lakukan aritmetik dalam pengendali tatal sahaja. Tangguhkan pemaparan ke
requestAnimationFramedan lindungi daripada penjadualan pendua; - Kekalkan penampan kecil. Lima hingga sepuluh baris pada setiap sisi sudah cukup; menggandakan penampan menggandakan nod DOM dan kerja gaya.
Dengan viewport 800 piksel dan baris 24 piksel, kira-kira 34 baris kelihatan, dan walaupun dengan penampan anda memaparkan sekitar lima puluh. Dokumen di bawah boleh memegang seratus ribu baris manakala DOM secara konsisten memegang beberapa ratus elemen, memastikan kos susun atur rata.
5. Runtuh, kembang, dan penambatan tatal
Sebaik indeks baris menjadi satu-satunya koordinat, beberapa butiran interaksi mula pecah:
- Kekalkan sauh visual semasa meruntuhkan di atas viewport. Baris di bawah beralih ke atas; jika
scrollToptidak diselaraskan dalam komit yang sama, kandungan kelihatan melompat. Rekod laluan baris sauh dan pulihkan kedudukan tatal menggunakan indeks baharunya. - Kembangkan nenek moyang sebelum melompat. Apabila carian menemui medan dalam, kembangkan setiap induk dahulu, kemudian kira indeks baris sasaran, kemudian tatal dan serlahkan. Langkau langkah pertama dan baris sasaran tidak berada dalam tatasusunan kelihatan.
- Pandu navigasi papan kekunci daripada baris kelihatan. Atas dan bawah menggerakkan indeks baris; kiri dan kanan mengembang dan meruntuhkan. Jangan sekali-kali berjalan seluruh pokok dalam pengendali kekunci.
- Kelompokkan kemas kini menjadi satu bingkai. Apabila mengembangkan beberapa tahap berturut-turut, cantumkan perubahan tatasusunan dan paparkan sekali, atau setiap tahap mencetuskan reflow penuh.
6. Carian dan penyerlahan: kira sekali, papar sedikit
Carian ialah perangkap prestasi paling biasa terlepas pandang dalam paparan pokok. Jika setiap ketukan kekunci berjalan seluruh graf objek dan membina semula senarai hasil, dokumen seratus ribu nod akan membeku serta-merta.
Pendekatan berlapis:
- Debounce input selama 150-250ms dan tandakan setiap carian dengan nombor jujukan supaya hasil basi dibuang;
- Imbas dalam Worker. Berjalan graf objek ialah pengiraan tulen tanpa kebergantungan DOM, jadi ia tidak sepatutnya menyekat menaip;
- Simpan laluan dan indeks baris sahaja. Jangan salin setiap nod yang sepadan atau pra-bina HTML yang diserlahkan;
- Serlahkan hanya baris kelihatan. Viewport memegang beberapa puluh baris; gunakan gaya mengikut laluan yang sepadan semasa pemaparan;
- Guna semula "kembangkan nenek moyang, kemudian cari indeks" untuk lompatan, laluan sama yang digunakan dengan mengklik chevron.
Satu butiran keselamatan: kunci dan nilai dalam paparan pokok ialah data pengguna. Membina HTML yang diserlahkan daripada mereka memerlukan pengescapean, atau string yang mengandungi markup menjadi XSS. Paparan JSON tidak memerlukan teks kaya, jadi nod teks serta nama kelas kedua-duanya lebih selamat dan lebih pantas.
7. Penyerlahan sintaks: CodeMirror dan Monaco ialah alat berbeza
Paparan pokok dan editor teks ialah model pemaparan berbeza, jadi "mana lebih pantas" bukan perbandingan langsung:
| Dimensi | CodeMirror 6 | Monaco Editor |
|---|---|---|
| Kedudukan | Teras editor modular | Editor IDE penuh |
| Pemaparan | Berasaskan viewport | Berasaskan viewport, tetapi model dan hiasan lebih berat |
| Penghuraian | Penghuraian inkremental Lezer atas julat yang berubah | Tokenizer Monarch, baris demi baris |
| Fail besar | Bergantung pada sambungan; ringan secara lalai | Lengkap ciri, memori dan kos permulaan lebih tinggi |
| Lipatan dan carian | Sambungan berasingan, dimuatkan mengikut permintaan | Terbina dalam, dengan overhed kekal |
| Perangkap biasa | Terlalu banyak sambungan memulihkan kos | Minimap, tatal melekat, dan hiasan bertindan |
Dua kesimpulan praktikal:
- Paparan pokok tidak memerlukan tokenizer penuh. Beri kunci, string, nombor, dan boolean satu warna masing-masing dengan nod teks biasa dan kelas CSS; itu menelan kos satu urutan magnitud kurang daripada menjalankan penyerlahan sintaks;
- Jangan pasang paparan pokok dan editor pada masa yang sama. Menyelenggara dua model untuk satu dokumen menggandakan memori; musnahkan paparan lama dan bukannya menyembunyikannya.
Jika editor benar-benar patut berada di dalam paparan, CodeMirror 6 sesuai untuk "baca sahaja, fail besar, penyerlahan setempat," manakala Monaco sesuai untuk pelengkapan, diagnostik, dan penyuntingan berbilang kursor. Ukur masa cat pertama dan memori mastautin sebelum membaca senarai ciri. Untuk perbandingan editor dan alat CLI yang lebih mendalam, lihat Editor, Alat Baris Perintah, dan Automasi.
8. Sempadan Worker: pindahkan penghuraian dan pembinaan baris dari main thread
Separuh lagi kesesakan pemaparan ialah penghuraian pada main thread. Untuk JSON melebihi 1MB, penghuraian, pemformatan, dan pembinaan baris rata patut berada dalam Web Worker; main thread hanya mengambil baris dan mengecat viewport.
Worker tidak percuma: postMessage menggunakan klon berstruktur, jadi menghantar graf objek 50MB kembali ke main thread membina salinan kedua dan menggandakan puncak. Sempadan yang betul ialah hurai dan ratakan di dalam Worker, pulangkan hanya baris yang diperlukan pemapar:
worker.onmessage = (event: MessageEvent<{ rows: TreeRow[] }>) => {
rows = event.data.rows;
requestAnimationFrame(() => renderViewport());
};
worker.postMessage({ text });
Jika data baris masih besar, mampatkan baris menjadi tatasusunan selari (laluan, jenis, offset nilai), letakkan dalam ArrayBuffer, dan pindahkan pemilikan supaya main thread tidak membayar kos salinan. Kosnya ialah menulis pensirian sendiri, yang hanya berbaloi pada berjuta-juta baris.
Peraturan untuk sempadan itu mudah: main thread sepatutnya hanya melakukan kerja yang mesti menyentuh DOM. Penghuraian, carian, pengisihan, pemformatan, dan pembezaan semuanya boleh berpindah. Pemaparan, tatal, input, dan fokus kekal pada main thread, dan setiap bingkai mesti memenuhi bajet kerja.
9. Metrik dan urutan pelaksanaan
Pengoptimuman tidak boleh berhenti pada "ia terasa lebih pantas." Set berikut boleh terus masuk ke dalam senarai semak penerimaan:
| Metrik | Sasaran | Cara mengukur |
|---|---|---|
| Bingkai kelihatan pertama | Di bawah 300ms | performance.now() dari buka hingga cat pertama |
| Tugas panjang | Tiada yang melebihi 50ms | PerformanceObserver memerhati longtask |
| Masa bingkai tatal | Mantap di bawah 16.7ms | Rakaman DevTools Performance |
| Elemen DOM dalam viewport | Di bawah 2,000 | document.querySelectorAll('*').length |
| Respons kembang / runtuh | Di bawah 50ms | Cap masa sekitar interaksi |
| Carian lebih 100,000 baris | Di bawah 300ms, dalam Worker | Masa dalam Worker dan pulangkan tempoh |
| Heap mastautin | Di bawah 3x saiz teks | Snapshot heap DevTools Memory |
Ukur dengan fail sebenar dan bukannya objek kecil yang dijana seragam; bentuk data penting, dan string panjang, bersarang dalam, serta tatasusunan sangat lebar semuanya menghasilkan lengkung berbeza. Untuk lejar graf objek itu sendiri, lihat Lejar Memori JSON.
Laksanakan dalam urutan ini:
- Ukur garis dasar: masa paparan pertama, bilangan nod, masa bingkai tatal;
- Runtuh secara lalai: kembangkan dua tahap, tunjukkan jenis dan kiraan dalam ringkasan;
- Senarai maya: ketinggian baris tetap, kunci stabil, viewport serta penampan kecil;
- Pindahkan pengiraan: penghuraian, carian, dan perataan ke dalam Worker;
- Hadkan penyerlahan: gaya baris kelihatan sahaja, elakkan mengecat semula seluruh pokok;
- Tambah pagar keselamatan: melebihi ambang, tawarkan "tunjukkan N baris pertama" atau perlukan penapis carian.
Setiap langkah berkongsi satu matlamat: kurangkan nod dan objek yang mesti wujud pada saat yang sama, dan bukannya meminta pelayar bekerja lebih keras pada mereka. Latar belakang penuh strategi berlapis ada dalam Menghurai, Memaparkan, dan Menstrim JSON Besar.
Bolehkah tatal maya membaiki kesesakan semasa penghuraian?
Tidak. Tatal maya hanya mengurangkan nod DOM. Jika penghuraian masih berlaku pada main thread, halaman masih tersekat-sekat. Pindahkan penghuraian ke dalam Worker untuk dokumen melebihi 1MB dan layan dua masalah itu secara berasingan.
Semuanya dikembangkan dan ia masih tersekat-sekat. Apa yang salah?
Sama ada senarai maya tidak benar-benar aktif, atau setiap pengembangan mengimbas semula seluruh pokok. Semak sama ada baris luar skrin masih mencipta DOM, dan sama ada pengembangan hanya mengira semula subtree terjejas.
CodeMirror atau Monaco untuk paparan JSON?
Utamakan CodeMirror 6 untuk baca sahaja, fail besar, dan penyerlahan setempat; pemuatan modular mengikut permintaannya lebih ringan. Pilih Monaco apabila anda memerlukan pelengkapan, diagnostik, dan penyuntingan berbilang kursor. Dengan mana-mana satu, elakkan memasang paparan pokok dan editor untuk dokumen yang sama sekali gus.
Mengapa kedudukan tatal salah selepas lompatan carian?
Biasanya kerana paparan menatal sebelum mengembangkan. Kembangkan semua nenek moyang dahulu, kemudian tetapkan kedudukan tatal daripada indeks baris baharu, dan serlahkan terakhir. Kunci baris mengikut laluan dan bukannya indeks supaya pengembangan tidak mengalihkan setiap indeks.
FAQ
Bagaimana saya mengetahui peringkat mana kesesakan itu datang?
Pecahkan "perlahan" kepada penghuraian dan pembinaan objek, penciptaan nod DOM, susun atur dan cat, serta respons interaksi, dan masa setiap peringkat dengan performance.now() dari buka hingga cat pertama. Kemudian perhatikan longtask dengan PerformanceObserver — mana-mana tugas melebihi 50ms menggugurkan bingkai, dan rakaman DevTools Performance menunjukkan sama ada masa bingkai tatal kekal di bawah 16.7ms.
Mengapa memaparkan seluruh pokok sekali gus mengosongkan skrin?
Dokumen 5MB ialah kira-kira 125,000 baris, dan paparan pokok biasanya menukar setiap baris menjadi 3 hingga 6 nod DOM — sekitar 500,000 elemen sekali gus. Setiap elemen juga membawa peraturan gaya, kotak susun atur, dan arahan cat, jadi beberapa tugas main thread melebihi 50ms berturut-turut membuat halaman kelihatan mati. Nod yang disembunyikan oleh induk runtuh terus menelan kerja susun atur, jadi "paparkan semuanya, kemudian sembunyikan" bukan pengoptimuman.
Patutkah saya menghantar graf objek 50MB kembali dari Worker?
Tidak. postMessage menggunakan klon berstruktur, jadi sisi penerima membina salinan kedua dan puncak berganda. Sempadan yang betul ialah menghurai dan meratakan di dalam Worker dan memulangkan hanya baris yang diperlukan pemapar; pada berjuta-juta baris anda boleh mengemas tatasusunan selari (laluan, jenis, offset nilai) ke dalam ArrayBuffer dan memindahkan pemilikan.