Keadaan Pekerja Web Pada Tahun 2021

Saya lelah selalu membandingkan web dengan apa yang disebut platform “asli” seperti Android dan iOS. Web sedang streaming, artinya tidak ada sumber daya yang tersedia secara lokal saat Anda membuka aplikasi untuk pertama kalinya. Ini adalah perbedaan mendasar , sehingga banyak pilihan arsitektur dari platform asli tidak mudah diterapkan ke web — jika sama sekali.

Tetapi di mana pun Anda melihat, multithreading digunakan di mana-mana . iOS memberdayakan pengembang untuk memparalelkan kode dengan mudah menggunakan Grand Central Dispatch , Android melakukan ini melalui WorkManager penjadwal tugas terpadu yang baru dan mesin game seperti Unity memiliki sistem pekerjaan . Alasan mengapa salah satu platform ini tidak hanya mendukung multithreading, tetapi membuatnya semudah mungkin selalu sama: Pastikan aplikasi Anda terasa hebat.

Dalam artikel ini saya akan menguraikan model mental saya mengapa multithreading penting di web, saya akan memberi Anda pengantar primitif yang kami miliki sebagai pengembang, dan saya akan berbicara sedikit tentang arsitektur yang membuatnya mudah untuk mengadopsi multithreading, bahkan secara bertahap.

Masalah Performa Tak Terduga

Tujuannya adalah untuk menjaga aplikasi Anda tetap lancar dan responsif. Smooth artinya memiliki frame rate yang stabil dan cukup tinggi. Responsif berarti bahwa UI merespons interaksi pengguna dengan penundaan minimal. Kedua hal ini adalah faktor kunci dalam membuat aplikasi Anda terasa halus dan berkualitas tinggi.

Menurut RAIL , menjadi responsif berarti bereaksi terhadap tindakan pengguna dalam waktu kurang dari 100 md, dan menjadi halus berarti mengirimkan 60 frame per detik (fps) yang stabil saat apa pun di layar bergerak. Akibatnya, kami sebagai pengembang memiliki 1000ms/60 = 16.6ms untuk menghasilkan setiap frame, yang juga disebut "anggaran bingkai".

Saya katakan "kami" , tetapi sebenarnya browser yang memiliki 16.6ms untuk melakukan semua yang diperlukan untuk membuat bingkai. Pengembang kami hanya bertanggung jawab langsung atas satu bagian dari beban kerja yang harus ditangani oleh browser. Pekerjaan itu terdiri dari (namun tidak terbatas pada):

  • Mendeteksi elemen mana yang mungkin atau mungkin tidak diketuk pengguna;
  • menembakkan peristiwa yang sesuai;
  • menjalankan event handler JavaScript terkait;
  • menghitung gaya;
  • melakukan tata letak;
  • lapisan lukisan;
  • dan menggabungkan lapisan-lapisan itu ke dalam gambar akhir yang dilihat pengguna di layar;
  • (dan banyak lagi …)

    Cukup banyak pekerjaan.

Pada saat yang sama, kami memiliki kesenjangan kinerja yang melebar . Ponsel unggulan papan atas semakin cepat dengan setiap generasi baru yang dirilis. Ponsel kelas bawah di sisi lain semakin murah , membuat internet seluler dapat diakses oleh demografi yang sebelumnya mungkin tidak mampu membelinya. Dalam hal kinerja, ponsel ini telah menyamai kinerja iPhone 2012.

Aplikasi yang dibuat untuk Web diharapkan dapat berjalan pada perangkat yang berada di mana saja pada spektrum kinerja yang luas ini. Berapa lama bagian JavaScript Anda selesai tergantung pada seberapa cepat perangkat menjalankan kode Anda. Tidak hanya itu, durasi tugas browser lainnya seperti layout dan paint juga dipengaruhi oleh karakteristik kinerja perangkat. Apa yang membutuhkan 0,5ms pada iPhone modern mungkin membutuhkan 10ms pada Nokia 2. Kinerja perangkat pengguna benar-benar tidak dapat diprediksi.

Catatan : RAIL telah menjadi kerangka panduan selama 6 tahun sekarang. Penting untuk dicatat bahwa 60fps benar-benar merupakan nilai pengganti untuk berapa pun kecepatan refresh asli tampilan pengguna. Misalnya, beberapa ponsel piksel yang lebih baru memiliki layar 90Hz dan iPad Pro memiliki layar 120Hz, mengurangi anggaran bingkai masing-masing menjadi 11.1ms dan 8.3ms.

Untuk memperumit masalah lebih lanjut, tidak ada cara yang baik untuk menentukan kecepatan refresh perangkat yang menjalankan aplikasi Anda selain mengukur jumlah waktu yang berlalu antara requestAnimationFrame() .*

JavaScript

JavaScript dirancang untuk berjalan dalam langkah kunci dengan loop rendering utama browser. Hampir setiap aplikasi web di luar sana bergantung pada model ini. Kelemahan dari desain itu adalah bahwa sejumlah kecil kode JavaScript yang lambat dapat mencegah pengulangan rendering browser untuk melanjutkan. Mereka saling berhadapan: jika yang satu tidak selesai, yang lain tidak bisa melanjutkan. Untuk memungkinkan tugas yang berjalan lebih lama untuk diintegrasikan ke dalam JavaScript, model asinkronisitas dibuat berdasarkan panggilan balik dan janji berikutnya.

Agar aplikasi Anda tetap lancar , Anda perlu memastikan bahwa kode JavaScript Anda digabungkan dengan tugas lain yang harus dilakukan browser (gaya, tata letak, cat,…) tidak menambahkan durasi yang lebih lama dari anggaran bingkai perangkat . Agar aplikasi tetap responsif , Anda perlu memastikan bahwa pengendali peristiwa tertentu tidak memerlukan waktu lebih dari 100 md agar dapat menampilkan perubahan di layar perangkat. Mencapai ini di perangkat Anda sendiri selama pengembangan bisa jadi sulit, tetapi mencapai ini di setiap perangkat yang mungkin menjalankan aplikasi Anda sepertinya tidak mungkin.

Saran yang biasa diberikan di sini adalah "memotong kode Anda" atau frasa saudaranya "menghasilkan browser". Prinsip dasarnya sama: Untuk memberi browser kesempatan mengirimkan frame berikutnya, Anda memecah pekerjaan yang dilakukan kode Anda menjadi potongan-potongan yang lebih kecil, dan memberikan kontrol kembali ke browser untuk memungkinkannya melakukan pekerjaan di antara potongan-potongan itu.

Ada banyak cara untuk menyerah pada browser, dan tidak ada yang bagus. API penjadwal tugas yang baru-baru ini diusulkan bertujuan untuk mengekspos fungsionalitas ini secara langsung. Namun, bahkan jika kami memiliki API untuk menghasilkan seperti await yieldToBrowser() (atau semacamnya), teknik itu sendiri cacat: Untuk memastikan Anda tidak menghabiskan anggaran bingkai Anda, Anda perlu melakukan pekerjaan dalam jumlah yang cukup kecil potongan yang dihasilkan kode Anda setidaknya sekali setiap frame.

Pada saat yang sama, kode yang terlalu sering menghasilkan dapat menyebabkan overhead tugas penjadwalan menjadi pengaruh negatif bersih pada kinerja keseluruhan aplikasi Anda. Sekarang gabungkan itu dengan kinerja perangkat yang tidak dapat diprediksi, dan kita harus sampai pada kesimpulan bahwa tidak ada ukuran potongan yang benar yang cocok untuk semua perangkat. Ini terutama bermasalah ketika mencoba "memotong" pekerjaan UI, karena menyerah pada browser dapat membuat sebagian antarmuka lengkap yang meningkatkan total biaya tata letak dan pengecatan.

Pekerja Web

Ada cara untuk berhenti berjalan di langkah kunci dengan utas rendering browser. Kami dapat memindahkan beberapa kode kami ke utas yang berbeda. Setelah berada di utas yang berbeda, kami dapat memblokir keinginan hati kami dengan JavaScript yang berjalan lama, tanpa kerumitan dan biaya chunking dan hasil, dan utas rendering bahkan tidak akan menyadarinya. Primitif untuk melakukan itu di web disebut pekerja web . Pekerja web dapat dibuat dengan meneruskan jalur ke file JavaScript terpisah yang akan dimuat dan dijalankan di utas yang baru dibuat ini:

 const worker = new Worker("./worker.js");

Sebelum kita membahas lebih dalam, penting untuk dicatat bahwa Pekerja Web, Pekerja Layanan, dan Worklet serupa, tetapi pada akhirnya berbeda untuk tujuan yang berbeda:

  • Dalam artikel ini, saya secara eksklusif berbicara tentang WebWorkers (seringkali hanya "Pekerja" untuk jangka pendek). Pekerja adalah lingkup JavaScript terisolasi yang berjalan di utas terpisah. Itu muncul (dan dimiliki) oleh sebuah halaman.
  • ServiceWorker adalah lingkup JavaScript terisolasi yang berumur pendek yang berjalan di utas terpisah, berfungsi sebagai proxy untuk setiap permintaan jaringan yang berasal dari halaman asal yang sama. Pertama dan terpenting, ini memungkinkan Anda untuk menerapkan perilaku caching kompleks yang sewenang-wenang, tetapi juga telah diperluas untuk memungkinkan Anda memanfaatkan pengambilan latar belakang yang berjalan lama, pemberitahuan push, dan fungsi lain yang memerlukan kode untuk dijalankan tanpa halaman terkait. Ini sangat mirip dengan Web Worker, tetapi dengan tujuan khusus dan batasan tambahan.
  • Worklet adalah cakupan JavaScript terisolasi dengan API yang sangat terbatas yang mungkin atau mungkin tidak berjalan di utas terpisah. Maksud dari worklet adalah bahwa browser dapat memindahkan worklet di antara utas. AudioWorklet , CSS Painting API dan Animation Worklet adalah contoh Worklet.
  • SharedWorker adalah Web Worker khusus, di mana beberapa tab atau jendela dengan asal yang sama dapat merujuk pada SharedWorker yang sama . API hampir tidak mungkin untuk polyfill dan hanya pernah diterapkan di Blink, jadi saya tidak akan memperhatikannya di artikel ini.

Karena JavaScript dirancang untuk berjalan dalam langkah kunci dengan browser, banyak API yang diekspos ke JavaScript tidak aman untuk thread, karena tidak ada konkurensi yang harus ditangani. Agar struktur data menjadi thread-safe berarti dapat diakses dan dimanipulasi oleh banyak utas secara paralel tanpa statusnya rusak.

Ini biasanya dicapai dengan mutex yang mengunci utas lain saat satu utas melakukan manipulasi. Tidak harus berurusan dengan kunci memungkinkan browser dan mesin JavaScript untuk membuat banyak optimasi untuk menjalankan kode Anda lebih cepat. Di sisi lain, ini memaksa pekerja untuk berjalan dalam lingkup JavaScript yang sepenuhnya terisolasi, karena segala bentuk berbagi data akan mengakibatkan masalah karena kurangnya keamanan utas.

Meskipun Pekerja adalah "utas" primitif web, mereka sangat berbeda dari utas yang mungkin Anda gunakan dari C++, Java & co. Perbedaan terbesar adalah bahwa isolasi yang diperlukan berarti pekerja tidak memiliki akses ke variabel atau kode apa pun dari halaman yang membuatnya atau sebaliknya. Satu-satunya cara untuk bertukar data adalah melalui pengiriman pesan melalui API yang disebut postMessage , yang akan menyalin muatan pesan dan memicu message di pihak penerima. Ini juga berarti bahwa Pekerja tidak memiliki akses ke DOM, membuat pembaruan UI dari seorang pekerja menjadi tidak mungkin — setidaknya tanpa usaha yang signifikan (seperti pekerja-dom AMP).

Dukungan untuk Pekerja Web hampir universal, mengingat bahkan IE10 mendukungnya. Penggunaannya, di sisi lain, masih relatif rendah, dan saya pikir sebagian besar disebabkan oleh ergonomi Pekerja yang tidak biasa.

Model Konkurensi JavaScript

Aplikasi apa pun yang ingin menggunakan Pekerja harus menyesuaikan arsitekturnya untuk mengakomodasi persyaratan Pekerja. JavaScript sebenarnya mendukung dua model konkurensi yang sangat berbeda yang sering dikelompokkan dalam istilah "Arsitektur Off-Main-Thread". Keduanya menggunakan Pekerja, tetapi dengan cara yang sangat berbeda dan masing-masing membawa set pengorbanan mereka sendiri. Aplikasi apa pun biasanya berakhir di suatu tempat di antara dua ekstrem ini.

Model Konkurensi #1: Aktor

Preferensi pribadi saya adalah memikirkan Pekerja seperti Aktor, seperti yang dijelaskan dalam Model Aktor . Inkarnasi Model Aktor yang paling populer mungkin dalam bahasa pemrograman Erlang. Setiap aktor mungkin atau mungkin tidak berjalan pada utas terpisah dan sepenuhnya memiliki data yang dioperasikannya. Tidak ada utas lain yang dapat mengaksesnya, membuat mekanisme sinkronisasi rendering seperti mutex tidak diperlukan. Aktor hanya dapat mengirim pesan satu sama lain dan bereaksi terhadap pesan yang mereka terima.

Sebagai contoh, saya sering menganggap utas utama sebagai aktor yang memiliki DOM dan akibatnya semua UI. Ini bertanggung jawab untuk memperbarui UI dan menangkap peristiwa input. Faktor lain mungkin bertanggung jawab atas status aplikasi. Aktor DOM mengonversi kejadian input tingkat rendah menjadi kejadian semantik tingkat aplikasi dan mengirimkannya ke aktor status. Aktor negara mengubah objek negara sesuai dengan peristiwa yang diterimanya, berpotensi menggunakan mesin negara atau bahkan melibatkan aktor lain. Setelah objek status diperbarui, ia mengirimkan salinan objek status yang diperbarui ke aktor DOM. Aktor DOM sekarang memperbarui DOM sesuai dengan objek status baru. Paul Lewis dan saya pernah menjelajahi arsitektur aplikasi yang berpusat pada aktor di Chrome Dev Summit 2018.

Tentu saja, model ini tidak datang tanpa masalah. Misalnya, setiap pesan yang Anda kirim perlu disalin. Berapa lama waktu yang dibutuhkan tidak hanya tergantung pada ukuran pesan, tetapi juga pada perangkat yang menjalankan aplikasi. Dalam pengalaman saya, postMessage biasanya “cukup cepat” , tetapi ada skenario tertentu yang tidak. Masalah lain adalah untuk mencapai keseimbangan antara memindahkan kode ke pekerja untuk membebaskan utas utama, sementara pada saat yang sama harus membayar biaya komunikasi dan pekerja sibuk menjalankan kode lain sebelum dapat menanggapi pesan Anda. Jika dilakukan tanpa perawatan, pekerja dapat berdampak negatif pada responsivitas UI.

Pesan yang dapat Anda kirim melalui postMessage cukup kompleks. Algoritme yang mendasarinya (disebut "klon terstruktur") dapat menangani struktur data melingkar dan bahkan Map dan Set . Namun, itu tidak dapat menangani fungsi atau kelas, karena kode tidak dapat dibagikan di seluruh cakupan dalam JavaScript. Agak menjengkelkan, mencoba memposting fungsi akan menimbulkan kesalahan, sementara kelas hanya akan secara diam-diam dikonversi ke objek JavaScript biasa, kehilangan metode dalam proses (detail di balik ini masuk akal tetapi akan meniup ruang lingkup artikel ini).

Selain itu, postMessage adalah mekanisme pesan api-dan-lupakan tanpa pemahaman bawaan tentang permintaan dan tanggapan. Jika Anda ingin menggunakan mekanisme permintaan/tanggapan (dan menurut pengalaman saya, sebagian besar arsitektur aplikasi pasti akan mengarahkan Anda ke sana), Anda harus membuatnya sendiri. Itu sebabnya saya menulis Comlink , yang merupakan perpustakaan yang menggunakan protokol RPC di bawah tenda untuk membuatnya tampak seperti objek dari pekerja dapat diakses dari utas utama dan sebaliknya. Saat menggunakan Comlink, Anda tidak harus berurusan dengan postMessage sama sekali. Satu-satunya artefak adalah karena sifat postMessage yang asinkron, fungsi tidak mengembalikan hasilnya, tetapi menjanjikannya. Menurut pendapat saya, ini memberi Anda yang terbaik dari Model Aktor dan Konkurensi Memori Bersama.

Comlink tidak ajaib, masih harus menggunakan postMessage untuk protokol RPC. Jika aplikasi Anda menjadi salah satu kasus yang jarang terjadi di mana postMessage menjadi hambatan, ada baiknya mengetahui bahwa ArrayBuffers dapat ditransfer . Mentransfer ArrayBuffer hampir instan dan melibatkan transfer kepemilikan yang tepat: Lingkup pengiriman JavaScript kehilangan akses ke data dalam proses. Saya menggunakan trik ini ketika saya bereksperimen dengan menjalankan simulasi fisika dari aplikasi WebVR dari utas utama .

Model Konkurensi #2: Memori Bersama

Seperti yang saya sebutkan di atas, pendekatan tradisional untuk threading didasarkan pada memori bersama. Pendekatan ini tidak layak dalam JavaScript karena hampir semua API telah dibangun dengan asumsi bahwa tidak ada akses bersamaan ke objek. Mengubah itu sekarang akan merusak web atau menimbulkan biaya kinerja yang signifikan karena sinkronisasi yang sekarang diperlukan. Sebaliknya, konsep memori bersama telah dibatasi pada satu jenis khusus: SharedArrayBuffer (atau disingkat SAB).

SAB, seperti ArrayBuffer, adalah sepotong memori linier yang dapat dimanipulasi menggunakan Typed Arrays atauDataViews . Jika SAB dikirim melalui postMessage, ujung lainnya tidak menerima salinan data , tetapi menangani potongan memori yang sama persis. Setiap perubahan yang dilakukan oleh satu utas terlihat oleh semua utas lainnya. Untuk memungkinkan Anda membangun mutex Anda sendiri dan struktur data konkuren lainnya, Atomics menyediakan segala macam utilitas untuk operasi atomik atau mekanisme menunggu thread-safe.

Kelemahan dari pendekatan ini datang dalam berbagai rasa. Pertama dan terpenting, itu hanya sepotong memori. Ini adalah primitif tingkat sangat rendah, memberi Anda banyak fleksibilitas dan kekuatan dengan biaya peningkatan upaya rekayasa dan pemeliharaan. Anda juga tidak memiliki cara langsung untuk mengerjakan objek dan larik JavaScript yang sudah Anda kenal. Ini hanya serangkaian byte.

Sebagai cara eksperimental untuk meningkatkan ergonomi di sini, saya menulis perpustakaan yang disebut objek yang didukung buffer yang mensintesis objek JavaScript yang mempertahankan nilainya ke buffer yang mendasarinya. Atau, WebAssembly menggunakan Worker dan SharedArrayBuffers untuk mendukung model threading C++ dan bahasa lainnya. Saya akan mengatakan WebAssembly saat ini menawarkan pengalaman terbaik untuk konkurensi memori bersama, tetapi juga mengharuskan Anda untuk meninggalkan banyak manfaat (dan kenyamanan) JavaScript di belakang dan membeli ke bahasa lain dan (biasanya) binari yang lebih besar diproduksi.

Studi Kasus: PROXX

Pada tahun 2019 , tim saya dan saya menerbitkan PROXX , klon Minesweeper berbasis web yang secara khusus menargetkan ponsel menengah. Ponsel berfitur memiliki resolusi kecil, biasanya tidak ada antarmuka sentuh, CPU yang kurang bertenaga, dan tidak ada GPU yang layak untuk dibicarakan. Terlepas dari semua keterbatasan ini, mereka semakin populer karena dijual dengan harga yang sangat rendah dan mereka termasuk browser web lengkap. Ini membuka web seluler untuk demografi yang sebelumnya tidak mampu membelinya.

Untuk memastikan bahwa gamenya responsif dan mulus bahkan di ponsel ini, kami menerapkan arsitektur seperti Aktor. Utas utama bertanggung jawab untuk merender DOM (melalui preact dan, jika tersedia, WebGL) dan menangkap peristiwa UI. Seluruh status aplikasi dan logika permainan berjalan di pekerja yang menentukan apakah Anda baru saja menginjak lubang hitam tambang dan, jika tidak, berapa banyak papan permainan yang akan diungkapkan. Logika permainan bahkan mengirimkan hasil antara ke utas UI untuk memberi pengguna pembaruan visual berkelanjutan.

Masa depan

Saya suka Model Aktor. Tetapi ergonomi JavaScript bersamaan tidak bagus secara keseluruhan. Banyak alat dibangun dan kode perpustakaan ditulis untuk membuatnya lebih baik, tetapi pada akhirnya JavaScript Bahasa perlu lebih baik di sini. Beberapa insinyur di TC39 menyukai topik ini dan mencoba mencari tahu bagaimana JavaScript dapat mendukung kedua model konkurensi dengan lebih baik. Beberapa proposal sedang dievaluasi, mulai dari mengizinkan kode untuk dipost-Message'd, hingga objek dibagikan di seluruh utas ke API tingkat yang lebih tinggi, seperti penjadwal seperti yang umum di platform asli.

Tak satu pun dari mereka telah mencapai tahap yang signifikan dalam proses standardisasi dulu, jadi saya tidak akan menghabiskan waktu di sini. Jika Anda penasaran, perhatikan proposal TC39 dan lihat apa yang dimiliki JavaScript generasi berikutnya.

Ringkasan

Pekerja adalah alat utama untuk menjaga utas utama tetap responsif dan lancar dengan mencegah kode yang berjalan lama secara tidak sengaja memblokir browser untuk dirender. Karena sifat komunikasi asinkron yang melekat dengan pekerja, adopsi pekerja memerlukan beberapa penyesuaian arsitektur di aplikasi web Anda, tetapi sebagai imbalannya Anda akan memiliki waktu yang lebih mudah untuk mendukung spektrum besar perangkat tempat web diakses.

Anda harus memastikan untuk mengadopsi arsitektur yang memungkinkan Anda memindahkan kode dengan mudah sehingga Anda dapat mengukur dampak kinerja arsitektur off-main-thread. Ergonomi pekerja web memiliki sedikit kurva belajar tetapi bagian yang paling rumit dapat diabstraksikan dengan perpustakaan seperti Comlink.

Sumber Daya Lebih Lanjut


FAQ

Ada beberapa pertanyaan dan pemikiran yang cukup sering muncul, jadi saya ingin mendahuluinya dan mencatat jawaban saya di sini.

Bukankah postMessage lambat?

Saran inti saya dalam semua masalah kinerja adalah: Ukur dulu! Tidak ada yang lambat (atau cepat) sampai Anda mengukur. Dalam pengalaman saya, bagaimanapun, postMessage biasanya "cukup cepat". Sebagai aturan praktis: Jika JSON.stringify(messagePayload) bawah 10KB, Anda hampir tidak berisiko membuat bingkai yang panjang, bahkan pada ponsel yang paling lambat sekalipun. Jika postMessage memang menjadi hambatan di aplikasi Anda, pertimbangkan teknik berikut:

  • Memecah pekerjaan Anda menjadi bagian-bagian yang lebih kecil sehingga Anda dapat mengirim pesan yang lebih kecil.
  • Jika pesan adalah objek status yang hanya sebagian kecil yang berubah, kirim patch (diffs) alih-alih seluruh objek.
  • Jika Anda mengirim banyak pesan, juga bermanfaat untuk mengelompokkan beberapa pesan menjadi satu.
  • Sebagai upaya terakhir, Anda dapat mencoba beralih ke representasi numerik dari pesan Anda dan mentransfer ArrayBuffers alih-alih mengirim pesan berbasis objek.

Manakah dari teknik ini yang benar tergantung pada konteksnya dan hanya dapat dijawab dengan mengukur dan mengisolasi kemacetan.

Saya ingin akses DOM dari Worker.

Yang ini banyak saya dapatkan. Namun, dalam kebanyakan skenario, itu hanya memindahkan masalah. Anda berisiko membuat utas utama ke-2 secara efektif, dengan semua masalah yang sama, hanya di utas yang berbeda. Membuat DOM aman untuk diakses dari banyak utas akan memerlukan penambahan kunci yang akan memperlambat operasi DOM. Ini mungkin akan merugikan banyak aplikasi web yang ada.

Selain itu, model langkah kunci memiliki manfaat . Ini memberi browser sinyal yang jelas pada jam berapa DOM dalam keadaan valid yang dapat ditampilkan ke layar. Di dunia DOM multi-utas, sinyal itu akan hilang dan kita harus berurusan dengan sebagian render atau artefak lainnya.

Saya sangat tidak suka harus meletakkan kode di file terpisah untuk Pekerja.

Saya setuju. Ada proposal yang sedang dievaluasi di TC39 untuk memasukkan modul ke modul lain tanpa semua kabel trip yang dimiliki Data URL dan Blob URL. Proposal ini juga memungkinkan Anda membuat pekerja tanpa memerlukan file terpisah. Jadi sementara saya tidak memiliki solusi yang memuaskan sekarang , iterasi JavaScript di masa mendatang pasti akan menghapus persyaratan ini.

June 30, 2021

codeorayo

Ampuh! Ini rahasia mengembangkan aplikasi secara instan, tinggal download dan kembangkan. Gabung sekarang juga! Premium Membership [PRIVATE] https://premium.codeorayo.com

Leave a Reply

Your email address will not be published. Required fields are marked *