Membuat Serialisasi Objek Kompleks dalam JavaScript

Diterbitkan: 2022-03-11

Kinerja Situs Web dan Caching Data

Situs web modern biasanya mengambil data dari sejumlah lokasi berbeda, termasuk database dan API pihak ketiga. Misalnya, saat mengautentikasi pengguna, situs web mungkin mencari catatan pengguna dari database, lalu menghiasinya dengan data dari beberapa layanan eksternal melalui panggilan API. Meminimalkan panggilan mahal ke sumber data ini, seperti akses disk untuk kueri database dan internet bolak-balik untuk panggilan API, sangat penting untuk mempertahankan situs yang cepat dan responsif. Caching data adalah teknik optimasi umum yang digunakan untuk mencapai hal ini.

Proses menyimpan data kerja mereka dalam memori. Jika server web berjalan dalam satu proses (seperti Node.js/Express), maka data ini dapat dengan mudah di-cache menggunakan cache memori yang berjalan dalam proses yang sama. Namun, server web dengan beban seimbang mencakup beberapa proses, dan bahkan saat bekerja dengan satu proses, Anda mungkin ingin cache tetap ada saat server dimulai ulang. Ini memerlukan solusi caching di luar proses seperti Redis, yang berarti data perlu diserialisasi, dan dideserialisasi saat dibaca dari cache.

Serialisasi dan deserialisasi relatif mudah dicapai dalam bahasa yang diketik secara statis seperti C#. Namun, sifat dinamis dari JavaScript membuat masalah ini sedikit lebih rumit. Saat ECMAScript 6 (ES6) memperkenalkan kelas, bidang pada kelas ini (dan tipenya) tidak ditentukan hingga diinisialisasi—yang mungkin tidak terjadi saat kelas digunakan—dan tipe kembalian bidang dan fungsi tidak ditentukan sama sekali dalam skema. Terlebih lagi, struktur kelas dapat dengan mudah diubah saat runtime—bidang dapat ditambahkan atau dihapus, jenis dapat diubah, dll. Meskipun ini dimungkinkan menggunakan refleksi dalam C#, refleksi mewakili "seni gelap" dari bahasa itu, dan pengembang mengharapkannya merusak fungsionalitas.

Saya dihadapkan dengan masalah ini di tempat kerja beberapa tahun yang lalu ketika bekerja di tim inti Toptal. Kami sedang membangun dasbor yang gesit untuk tim kami, yang harus cepat; jika tidak, pengembang dan pemilik produk tidak akan menggunakannya. Kami mengambil data dari sejumlah sumber: sistem pelacakan kerja kami, alat manajemen proyek kami, dan database. Situs ini dibuat di Node.js/Express, dan kami memiliki cache memori untuk meminimalkan panggilan ke sumber data ini. Namun, proses pengembangan kami yang cepat dan berulang berarti kami menerapkan (dan karenanya memulai kembali) beberapa kali sehari, membuat cache menjadi tidak valid dan dengan demikian kehilangan banyak manfaatnya.

Solusi yang jelas adalah cache di luar proses seperti Redis. Namun, setelah beberapa penelitian, saya menemukan bahwa tidak ada perpustakaan serialisasi yang baik untuk JavaScript. Metode JSON.stringify/JSON.parse bawaan mengembalikan data dari tipe objek, kehilangan fungsi apa pun pada prototipe kelas asli. Ini berarti objek deserialized tidak bisa begitu saja digunakan "di tempat" dalam aplikasi kita, yang karenanya akan membutuhkan refactoring yang cukup besar untuk bekerja dengan desain alternatif.

Persyaratan untuk Perpustakaan

Untuk mendukung serialisasi dan deserialisasi data arbitrer dalam JavaScript, dengan representasi deserial dan aslinya dapat digunakan secara bergantian, kami membutuhkan perpustakaan serialisasi dengan properti berikut:

  • Representasi deserialized harus memiliki prototipe yang sama (fungsi, getter, setter) sebagai objek aslinya.
  • Pustaka harus mendukung jenis kompleksitas bersarang (termasuk larik dan peta), dengan prototipe objek bersarang diatur dengan benar.
  • Harus dimungkinkan untuk membuat serial dan deserialize objek yang sama beberapa kali—prosesnya harus idempoten.
  • Format serialisasi harus mudah ditransmisikan melalui TCP dan dapat disimpan menggunakan Redis atau layanan serupa.
  • Perubahan kode minimal harus diperlukan untuk menandai kelas sebagai serial.
  • Rutinitas perpustakaan harus cepat.
  • Idealnya, harus ada beberapa cara untuk mendukung deserialisasi versi lama dari suatu kelas, melalui semacam pemetaan/versi.

Penerapan

Untuk menutup celah ini, saya memutuskan untuk menulis Tanagra.js , perpustakaan serialisasi tujuan umum untuk JavaScript. Nama perpustakaan adalah referensi ke salah satu episode favorit saya Star Trek: The Next Generation , di mana kru Enterprise harus belajar berkomunikasi dengan ras alien misterius yang bahasanya tidak dapat dipahami. Pustaka serialisasi ini mendukung format data umum untuk menghindari masalah seperti itu.

Tanagra.js dirancang untuk menjadi sederhana dan ringan, dan saat ini mendukung Node.js (belum diuji dalam browser, tetapi secara teori, seharusnya berfungsi) dan kelas ES6 (termasuk Maps). Implementasi utama mendukung JSON, dan versi eksperimental mendukung Buffer Protokol Google. Pustaka hanya memerlukan JavaScript standar (saat ini diuji dengan ES6 dan Node.js), tanpa ketergantungan pada fitur eksperimental, transpiling Babel , atau TypeScript .

Kelas yang dapat diserialisasi ditandai dengan pemanggilan metode saat kelas diekspor:

module.exports = serializable(Foo, myUniqueSerialisationKey)

Metode mengembalikan proxy ke kelas, yang memotong konstruktor dan menyuntikkan pengenal unik. (Jika tidak ditentukan, ini default ke nama kelas.) Kunci ini diserialisasi dengan sisa data, dan kelas juga mengeksposnya sebagai bidang statis. Jika kelas berisi tipe bersarang (yaitu, anggota dengan tipe yang membutuhkan serialisasi), mereka juga ditentukan dalam pemanggilan metode:

module.exports = serializable(Foo, [Bar, Baz], myUniqueSerialisationKey)

(Tipe bersarang untuk versi kelas sebelumnya juga dapat ditentukan dengan cara yang sama, sehingga, misalnya, jika Anda membuat serial Foo1, itu dapat dideserialisasi menjadi Foo2.)

Selama serialisasi, perpustakaan secara rekursif membangun peta global kunci ke kelas, dan menggunakannya selama deserialisasi. (Ingat, kuncinya diserialisasi dengan sisa data.) Untuk mengetahui jenis kelas "tingkat atas", perpustakaan mengharuskan ini ditentukan dalam panggilan deserialisasi:

const foo = decodeEntity(serializedFoo, Foo)

Pustaka pemetaan otomatis eksperimental berjalan di pohon modul dan menghasilkan pemetaan dari nama kelas, tetapi ini hanya berfungsi untuk kelas dengan nama unik.

Tata Letak Proyek

Proyek ini dibagi menjadi beberapa modul:

  • tanagra-core - fungsi umum yang dibutuhkan oleh format serialisasi yang berbeda, termasuk fungsi untuk menandai kelas sebagai serializable
  • tanagra-json - membuat serial data ke dalam format JSON
  • tanagra-protobuf - membuat serial data ke dalam format protobuffers Google (percobaan)
  • tanagra-protobuf-redis-cache - pustaka pembantu untuk menyimpan protobuf berseri di Redis
  • tanagra-auto-mapper - menjalankan pohon modul di Node.js untuk membuat peta kelas, artinya pengguna tidak perlu menentukan tipe yang akan dideserialisasi (eksperimental).

Perhatikan bahwa perpustakaan menggunakan ejaan AS.

Contoh Penggunaan

Contoh berikut mendeklarasikan kelas serializable dan menggunakan modul tanagra-json untuk membuat serial/deserialize-nya:

 const serializable = require('tanagra-core').serializable class Foo { constructor(bar, baz1, baz2, fooBar1, fooBar2) { this.someNumber = 123 this.someString = 'hello, world!' this.bar = bar // a complex object with a prototype this.bazArray = [baz1, baz2] this.fooBarMap = new Map([ ['a', fooBar1], ['b', fooBar2] ]) } } // Mark class `Foo` as serializable and containing sub-types `Bar`, `Baz` and `FooBar` module.exports = serializable(Foo, [Bar, Baz, FooBar]) ... const json = require('tanagra-json') json.init() // or: // require('tanagra-protobuf') // await json.init() const foo = new Foo(bar, baz) const encoded = json.encodeEntity(foo) ... const decoded = json.decodeEntity(encoded, Foo)

Pertunjukan

Saya membandingkan kinerja dua serializer (serializer JSON dan serializer protobufs eksperimental) dengan kontrol (JSON.parse asli dan JSON.stringify). Saya melakukan total 10 percobaan dengan masing-masing.

Saya menguji ini pada laptop Dell XPS15 2017 saya dengan memori 32Gb, menjalankan Ubuntu 17.10.

Saya membuat serial objek bersarang berikut:

 foo: { "string": "Hello foo", "number": 123123, "bars": [ { "string": "Complex Bar 1", "date": "2019-01-09T18:22:25.663Z", "baz": { "string": "Simple Baz", "number": 456456, "map": Map { 'a' => 1, 'b' => 2, 'c' => 2 } } }, { "string": "Complex Bar 2", "date": "2019-01-09T18:22:25.663Z", "baz": { "string": "Simple Baz", "number": 456456, "map": Map { 'a' => 1, 'b' => 2, 'c' => 2 } } } ], "bazs": Map { 'baz1' => Baz { string: 'baz1', number: 111, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } }, 'baz2' => Baz { string: 'baz2', number: 222, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } }, 'baz3' => Baz { string: 'baz3', number: 333, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } } }, }

Tulis Performa

Metode serialisasi jalan inc. percobaan pertama (ms) StDev. termasuk percobaan pertama (ms) jalan ex. percobaan pertama (ms) StDev. mantan. percobaan pertama (ms)
JSON 0,115 0,0903 0,0879 0,0256
Google Protobuf 2.00 2,748 1.13 0.278
Grup kontrol 0,0155 0,00726 0,0139 0,00570

Membaca

Metode serialisasi jalan inc. percobaan pertama (ms) StDev. termasuk percobaan pertama (ms) jalan ex. percobaan pertama (ms) StDev. mantan. percobaan pertama (ms)
JSON 0.133 0.102 0.104 0,0429
Google Protobuf 2.62 1.12 2.28 0,364
Grup kontrol 0,0135 0,00729 0,0115 0,00390

Ringkasan

Serializer JSON sekitar 6-7 kali lebih lambat dari serialisasi asli. Serializer protobufs eksperimental sekitar 13 kali lebih lambat dari serializer JSON , atau 100 kali lebih lambat dari serialisasi asli.

Selain itu, caching internal informasi skema/struktural dalam setiap serializer jelas memiliki efek pada kinerja. Untuk serializer JSON, penulisan pertama sekitar empat kali lebih lambat dari rata-rata. Untuk serializer protobuf, ini sembilan kali lebih lambat. Jadi menulis objek yang metadatanya telah di-cache jauh lebih cepat di kedua perpustakaan.

Efek yang sama diamati untuk bacaan. Untuk library JSON, pembacaan pertama sekitar empat kali lebih lambat dari rata-rata, dan untuk library protobuf, sekitar dua setengah kali lebih lambat.

Masalah kinerja serializer protobuf berarti masih dalam tahap percobaan, dan saya akan merekomendasikannya hanya jika Anda memerlukan format untuk beberapa alasan. Namun, ada baiknya menginvestasikan waktu, karena formatnya jauh lebih sederhana daripada JSON, dan karena itu lebih baik untuk mengirim melalui kabel. Stack Exchange menggunakan format untuk caching internalnya.

Serializer JSON jelas jauh lebih berkinerja tetapi masih jauh lebih lambat daripada implementasi asli. Untuk pohon objek kecil, perbedaan ini tidak signifikan (beberapa milidetik di atas permintaan 50 md tidak akan merusak kinerja situs Anda), tetapi ini bisa menjadi masalah untuk pohon objek yang sangat besar, dan merupakan salah satu prioritas pengembangan saya.

Peta jalan

Perpustakaan ini masih dalam tahap beta. Serializer JSON cukup teruji dan stabil. Berikut adalah peta jalan untuk beberapa bulan ke depan:

  • Peningkatan kinerja untuk kedua serializer
  • Dukungan yang lebih baik untuk JavaScript pra-ES6
  • Dukungan untuk dekorator ES-Next

Saya tahu tidak ada pustaka JavaScript lain yang mendukung serialisasi kompleks, data objek bersarang, dan deserializing ke tipe aslinya. Jika Anda menerapkan fungsionalitas yang akan mendapat manfaat dari perpustakaan, silakan mencobanya, hubungi umpan balik Anda, dan pertimbangkan untuk berkontribusi.

Beranda proyek
Repositori GitHub