Mempercepat grafik menggunakan OffscreenCanvas

Render grafik dapat berdampak serius pada browser. Terutama ketika datang untuk mengeluarkan banyak elemen yang mewakili diagram dalam antarmuka aplikasi yang kompleks. Anda dapat mencoba memperbaiki situasi menggunakan antarmuka OffscreenCanvas, tingkat dukungan yang browser secara bertahap tumbuh. Hal ini memungkinkan, menggunakan pekerja web, untuk menggeser tugas membentuk gambar yang ditampilkan dalam elemen yang terlihat <canvas>. Artikel, terjemahan yang kami terbitkan hari ini, dikhususkan untuk penggunaan antarmuka . Di sini kita akan berbicara tentang mengapa antarmuka ini mungkin diperlukan, tentang apa yang benar-benar dapat diharapkan dari penggunaannya, dan tentang kesulitan apa yang mungkin timbul ketika bekerja dengannya.



OffscreenCanvas

Mengapa memperhatikan OffscreenCanvas?


Kode yang terlibat dalam rendering diagram itu sendiri mungkin memiliki kompleksitas komputasi yang cukup tinggi. Di banyak tempat, Anda dapat menemukan cerita terperinci tentang cara menampilkan animasi yang halus dan untuk memastikan interaksi pengguna yang nyaman dengan halaman, perlu bahwa rendering satu frame cocok dalam waktu sekitar 10 ms, yang memungkinkan Anda mencapai 60 frame per detik. Jika perhitungan yang diperlukan untuk menghasilkan satu frame membutuhkan lebih dari 10 ms, ini akan menghasilkan "pelambatan" yang nyata dari halaman tersebut.

Saat menampilkan diagram, situasinya diperburuk oleh hal-hal berikut:

  • — - (SVG, canvas, WebGL) , , .
  • , , , , 10 . ( — ) .

Masalah-masalah ini adalah kandidat ideal untuk pendekatan optimasi klasik yang disebut divide and conquer. Dalam hal ini, kita berbicara tentang distribusi beban komputasi di beberapa utas. Namun, sebelum tampilan antarmuka, OffscreenCanvassemua kode rendering perlu dieksekusi di utas utama. Satu-satunya cara kode ini dapat menggunakan API yang diperlukan.

Dari sudut pandang teknis, perhitungan "berat" bisa menjadi output ke utas pekerja web sebelumnya. Tetapi, karena panggilan yang bertanggung jawab untuk rendering harus dilakukan dari aliran utama, ini membutuhkan penggunaan skema pertukaran pesan yang kompleks antara aliran pekerja dan aliran utama dalam kode output bagan. Selain itu, pendekatan ini sering hanya memberikan sedikit peningkatan kinerja.

SpesifikasiOffscreenCanvasmemberi kami mekanisme untuk mentransfer kontrol permukaan untuk menampilkan elemen grafik <canvas>di pekerja web. Spesifikasi ini saat ini didukung oleh browser Chrome dan Edge (setelah Edge dimigrasikan ke Chromium). Diharapkan bahwa dukungan Firefox OffscreenCanvasakan muncul dalam waktu enam bulan. Jika kita berbicara tentang Safari, masih belum diketahui apakah dukungan untuk teknologi ini direncanakan di browser ini.

Output bagan menggunakan OffscreenCanvas



Contoh bagan yang menampilkan 100.000 titik multi-warna

Untuk menilai lebih baik potensi keuntungan dan kerugiannyaOffscreenCanvas, mari kita lihat contoh output dari diagram "berat" yang ditunjukkan pada gambar sebelumnya.

const offscreenCanvas = canvasContainer
  .querySelector('canvas')
  .transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);

Pertama, kami kueri OffscreenCanvaselemen canvasmenggunakan metode baru transferControlToOffscreen(). Kemudian kami memanggil metode worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas]), melakukan ini untuk mengirim pesan kepada pekerja yang berisi tautan offscreenCanvas. Sangat penting bahwa di sini, sebagai argumen kedua, Anda perlu menggunakan [offscreenCanvas]. Ini memungkinkan Anda untuk membuat pemilik objek pekerja ini, yang akan memungkinkannya untuk mengelola sendiri OffscreenCanvas.

canvasContainer.addEventListener('measure', ({ detail }) => {
  const { width, height } = detail;
  worker.postMessage({ width, height });
});
canvasContainer.requestRedraw();

Mempertimbangkan bahwa ukuran OffscreenCanvasawalnya diturunkan dari atribut widthdan heightelemen canvas, adalah tanggung jawab kami untuk menjaga nilai-nilai ini tetap mutakhir ketika elemen diubah ukurannya canvas. Di sini kita menggunakan acara measuredari d3fc-canvas, yang akan memberi kita kesempatan, dalam perjanjian dengan requestAnimationFrame, untuk menyampaikan informasi kepada pekerja tentang dimensi baru elemen canvas.

Untuk menyederhanakan contoh, kita akan menggunakan komponen dari pustaka d3fc . Ini adalah seperangkat komponen tambahan yang menggabungkan komponen d3 atau melengkapi fungsinya. Anda OffscreenCanvasdapat bekerja dengan tanpa komponen-d3fc. Semua yang akan dibahas dapat dilakukan menggunakan fitur JavaScript standar eksklusif.

Sekarang buka kode dari file worker.js. Dalam contoh ini, demi peningkatan kinerja rendering yang nyata, kita akan menggunakan WebGL.

addEventListener('message', ({ data: { offscreenCanvas, width, height } }) => {
    if (offscreenCanvas != null) {
        const gl = offscreenCanvas.getContext('webgl');
        series.context(gl);
        series(data);
    }

    if (width != null && height != null) {
        const gl = series.context();
        gl.canvas.width = width;
        gl.canvas.height = height;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    }
});

Ketika kami menerima pesan yang berisi properti canvas, kami menganggap bahwa pesan itu berasal dari utas utama. Selanjutnya, kita mendapatkan canvaskonteks dari elemen webgldan meneruskannya ke komponen series. Kemudian kita memanggil komponen series, meneruskannya data ( data), yang ingin kita render menggunakannya (di bawah ini kita akan berbicara tentang dari mana variabel yang sesuai berasal ).

Selain itu, kami memeriksa properti widthdan height, yang datang dalam pesan, dan menggunakannya untuk mengatur dimensi offscreenCanvasdan area tampilan WebGL. Tautan offscreenCanvastidak digunakan secara langsung. Faktanya adalah bahwa pesan tersebut mengandung properti offscreenCanvasatau properti widthdan height.

Kode pekerja yang tersisa bertanggung jawab untuk mengatur rendering dari apa yang ingin kita hasilkan. Tidak ada yang istimewa di sini, jadi jika semua ini tidak asing bagi Anda, Anda dapat langsung melanjutkan ke bagian selanjutnya di mana kami membahas kinerja.

const randomNormal = d3.randomNormal(0, 1);
const randomLogNormal = d3.randomLogNormal();

const data = Array.from({ length: 1e5 }, () => ({
    x: randomNormal(),
    y: randomNormal(),
    size: randomLogNormal() * 10
}));

const xScale = d3.scaleLinear().domain([-5, 5]);

const yScale = d3.scaleLinear().domain([-5, 5]);

Pertama, kami membuat kumpulan data yang berisi koordinat titik yang didistribusikan secara acak di sekitar titik asal x / y, dan menyesuaikan penskalaan. Kami tidak menetapkan rentang nilai x dan y menggunakan metode ini range, karena seri WebGL ditampilkan dalam ukuran penuh elemen canvas (-1 -> +1dalam koordinat normalisasi perangkat).

const series = fc
    .seriesWebglPoint()
    .xScale(xScale)
    .yScale(yScale)
    .crossValue(d => d.x)
    .mainValue(d => d.y)
    .size(d => d.size)
    .equals((previousData, data) => previousData.length > 0);

Selanjutnya, kami menyiapkan serangkaian poin menggunakan xScaledan yScale. Kemudian kami mengonfigurasi cara akses yang sesuai yang memungkinkan Anda membaca data.

Selain itu, kami mendefinisikan fungsi pengecekan kesetaraan kami sendiri, yang dirancang untuk memastikan bahwa komponen tidak mengirimkan dataGPU pada setiap rendering. Kami harus mengungkapkan ini secara eksplisit, karena meskipun kami tahu bahwa kami tidak akan mengubah data ini, komponen tidak dapat mengetahuinya tanpa melakukan pemeriksaan "kotor" yang intensif sumber daya.

const colorScale = d3.scaleOrdinal(d3.schemeAccent);

const webglColor = color => {
    const { r, g, b, opacity } = d3.color(color).rgb();
    return [r / 255, g / 255, b / 255, opacity];
};

const fillColor = fc
    .webglFillColor()
    .value((d, i) => webglColor(colorScale(i)))
    .data(data);

series.decorate(program => {
    fillColor(program);
});

Kode ini memungkinkan Anda untuk mewarnai bagan. Kami menggunakan indeks titik dalam kumpulan data untuk memilih warna yang cocok colorScale, lalu mengonversinya ke format yang diperlukan dan menggunakannya untuk menghias titik keluaran.

function render() {
    const ease = 5 * (0.51 + 0.49 * Math.sin(Date.now() / 1e3));
    xScale.domain([-ease, ease]);
    yScale.domain([-ease, ease]);
    series(data);
    requestAnimationFrame(render);
}

Sekarang setelah titik-titik diwarnai, yang tersisa hanyalah menghidupkan grafik. Karena itu, kita perlu pemanggilan metode yang konstan render(). Ini, sebagai tambahan, akan membuat beberapa beban pada sistem. Kami mensimulasikan peningkatan dan penurunan skala grafik menggunakan requestAnimationFrameuntuk memodifikasi properti xScale.domaindan yScale.domainsetiap frame. Di sini, nilai tergantung waktu diterapkan dan dihitung sehingga skala bagan berubah dengan lancar. Selain itu, kami memodifikasi header pesan sehingga metode dipanggil untuk memulai siklus rendering render(), dan agar kami tidak harus langsung menelepon series(data).

importScripts(
    './node_modules/d3-array/dist/d3-array.js',
    './node_modules/d3-collection/dist/d3-collection.js',
    './node_modules/d3-color/dist/d3-color.js',
    './node_modules/d3-interpolate/dist/d3-interpolate.js',
    './node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js',
    './node_modules/d3-random/dist/d3-random.js',
    './node_modules/d3-scale/dist/d3-scale.js',
    './node_modules/d3-shape/dist/d3-shape.js',
    './node_modules/d3fc-extent/build/d3fc-extent.js',
    './node_modules/d3fc-random-data/build/d3fc-random-data.js',
    './node_modules/d3fc-rebind/build/d3fc-rebind.js',
    './node_modules/d3fc-series/build/d3fc-series.js',
    './node_modules/d3fc-webgl/build/d3fc-webgl.js'
);

Agar contoh ini berfungsi, tetap hanya mengimpor perpustakaan yang diperlukan ke dalam pekerja. Kami menggunakan ini importScripts, dan juga, agar tidak menggunakan alat untuk membangun proyek, kami menggunakan daftar dependensi yang dikompilasi secara manual. Sayangnya, kami tidak bisa hanya mengunduh build d3 / d3fc lengkap, karena mereka tergantung DOM, dan apa yang mereka butuhkan pada pekerja tidak tersedia.

→ Kode lengkap untuk contoh ini dapat ditemukan di GitHub

Kanal dan kinerja Offscreen


Animasi dalam proyek kami bekerja berkat penggunaan requestAnimationFramepekerja. Hal ini memungkinkan pekerja untuk membuat halaman bahkan ketika utas utama sibuk dengan hal-hal lain. Jika Anda melihat halaman proyek yang dijelaskan di bagian sebelumnya dan mengklik tombol Stop main thread, Anda dapat memperhatikan fakta bahwa ketika kotak pesan ditampilkan, pembaruan informasi cap waktu berhenti. Utas utama diblokir, tetapi animasi grafik tidak berhenti.


Jendela Proyek

Kita dapat mengatur rendering yang dimulai dengan menerima pesan dari aliran utama. Misalnya, peristiwa semacam itu dapat dikirim ketika pengguna berinteraksi dengan bagan atau ketika menerima data baru melalui jaringan. Tetapi dengan pendekatan ini, jika utas sibuk dengan sesuatu dan tidak ada pesan yang diterima pada pekerja, rendering akan berhenti.

Render bagan yang terus-menerus dalam kondisi ketika tidak ada yang terjadi di atasnya adalah cara yang bagus untuk mengganggu pengguna dengan kipas angin yang berdengung dan baterai lemah. Akibatnya, ternyata di dunia nyata, keputusan tentang bagaimana membagi tanggung jawab antara utas utama dan utas pekerja tergantung pada apakah pekerja dapat memberikan sesuatu yang bermanfaat ketika dia tidak menerima pesan tentang perubahan situasi dari utas utama.

Jika kita berbicara tentang transfer pesan di antara utas, harus dicatat bahwa di sini kita menerapkan pendekatan yang sangat sederhana. Sejujurnya, kita perlu mengirimkan sangat sedikit pesan di antara utas-utas, jadi saya lebih suka menyebut pendekatan kami sebagai konsekuensi pragmatisme, bukan kemalasan. Tetapi jika kita berbicara tentang aplikasi nyata, dapat dicatat bahwa skema transfer pesan antara aliran akan menjadi lebih rumit jika interaksi pengguna dengan diagram dan memperbarui data yang divisualisasikan akan bergantung pada mereka.

Anda dapat menggunakan antarmuka MessageChannel standar untuk membuat saluran pesan terpisah antara utas utama dan utas pekerja .. Masing-masing saluran ini dapat digunakan untuk kategori pesan tertentu, yang membuatnya lebih mudah untuk memproses pesan. Sebagai alternatif untuk mekanisme standar, perpustakaan pihak ketiga seperti Comlink dapat bertindak . Pustaka ini menyembunyikan detail tingkat rendah di belakang antarmuka tingkat tinggi menggunakan objek proxy .

Fitur lain yang menarik dari contoh kami, yang menjadi jelas terlihat dalam retrospeksi, adalah fakta bahwa memperhitungkan fakta bahwa sumber daya GPU, seperti sumber daya sistem lainnya, jauh dari tiada akhir. Terjemahan rendering menjadi pekerja memungkinkan utas untuk menyelesaikan masalah lainnya. Tetapi browser, bagaimanapun, perlu menggunakan GPU untuk membuat DOM dan apa yang dihasilkan oleh sarana OffscreenCanvas.

Jika pekerja mengkonsumsi semua sumber daya GPU, maka jendela aplikasi utama akan mengalami masalah kinerja, terlepas dari di mana rendering dilakukan. Perhatikan bagaimana kecepatan memperbarui stempel waktu dalam contoh menurun saat jumlah poin yang ditampilkan meningkat. Jika Anda memiliki kartu grafis yang kuat, maka Anda mungkin harus meningkatkan jumlah poin yang dikirimkan ke halaman dalam string kueri. Untuk melakukan ini, Anda dapat menggunakan nilai yang melebihi nilai maksimum 100000, ditentukan menggunakan salah satu tautan di halaman contoh.

Perlu dicatat bahwa di sini kita tidak menjelajahi dunia rahasia atribut konteks. Studi semacam itu dapat membantu kita meningkatkan produktivitas solusi. Namun, saya tidak melakukan ini dengan melakukannya karena rendahnya dukungan untuk atribut ini.

Jika kita berbicara tentang rendering menggunakan OffscreenCanvas, maka atributnya terlihat paling menarik di sini desynchronized. Itu, jika didukung, dan tunduk pada beberapa batasan, memungkinkan Anda untuk menyingkirkan sinkronisasi antara loop peristiwa dan loop rendering yang dijalankan pada pekerja. Ini meminimalkan keterlambatan memperbarui gambar. Detail tentang ini dapat ditemukan di sini .

Ringkasan


Antarmuka OffscreenCanvasmemberi para pengembang kesempatan untuk meningkatkan kinerja pembuatan grafik, tetapi menggunakannya membutuhkan pendekatan yang bijaksana. Dengan menggunakannya, Anda perlu mempertimbangkan hal berikut:

  • (, , ) - .

    • , . , , , postMessage.
  • , (, SVG/HTML- <canvas>), , , .

    • , , , , , , , , .
  • , GPU, OffscreenCanvas .

    • OffscreenCanvas, GPU-, , . , , .

Berikut adalah contoh kode, dan di sini adalah versi kerjanya.

Pembaca yang budiman! Sudahkah Anda menggunakan OffscreenCanvas untuk mempercepat tampilan grafik di halaman web?


All Articles