การจัดลำดับวัตถุที่ซับซ้อนใน JavaScript
เผยแพร่แล้ว: 2022-03-11ประสิทธิภาพของเว็บไซต์และการแคชข้อมูล
เว็บไซต์สมัยใหม่มักจะดึงข้อมูลจากสถานที่ต่างๆ รวมทั้งฐานข้อมูลและ API ของบุคคลที่สาม ตัวอย่างเช่น เมื่อตรวจสอบสิทธิ์ผู้ใช้ เว็บไซต์อาจค้นหาบันทึกผู้ใช้จากฐานข้อมูล แล้วประดับด้วยข้อมูลจากบริการภายนอกบางอย่างผ่านการเรียก API การลดจำนวนการโทรที่มีราคาแพงไปยังแหล่งข้อมูลเหล่านี้ เช่น การเข้าถึงดิสก์สำหรับการสืบค้นฐานข้อมูลและการไปกลับทางอินเทอร์เน็ตสำหรับการเรียก API เป็นสิ่งจำเป็นต่อการรักษาไซต์ที่รวดเร็วและตอบสนอง การแคชข้อมูลเป็นเทคนิคการเพิ่มประสิทธิภาพทั่วไปที่ใช้เพื่อให้บรรลุสิ่งนี้
กระบวนการจัดเก็บข้อมูลการทำงานในหน่วยความจำ หากเว็บเซิร์ฟเวอร์ทำงานในกระบวนการเดียว (เช่น Node.js/Express) ข้อมูลนี้สามารถแคชได้อย่างง่ายดายโดยใช้แคชหน่วยความจำที่ทำงานอยู่ในกระบวนการเดียวกัน อย่างไรก็ตาม เว็บเซิร์ฟเวอร์ที่มีโหลดบาลานซ์ครอบคลุมหลายกระบวนการ และแม้เมื่อทำงานกับกระบวนการเดียว คุณอาจต้องการให้แคชยังคงอยู่เมื่อเซิร์ฟเวอร์รีสตาร์ท สิ่งนี้จำเป็นสำหรับโซลูชันการแคชที่อยู่นอกกระบวนการ เช่น Redis ซึ่งหมายความว่าข้อมูลจำเป็นต้องได้รับการซีเรียลไลซ์อย่างใด และดีซีเรียลไลซ์เมื่ออ่านจากแคช
การทำให้เป็นอนุกรมและการดีซีเรียลไลซ์เซชันนั้นค่อนข้างตรงไปตรงมาในภาษาที่พิมพ์แบบสแตติก เช่น C# อย่างไรก็ตาม ลักษณะไดนามิกของ JavaScript ทำให้ปัญหายากขึ้นเล็กน้อย ในขณะที่ ECMAScript 6 (ES6) แนะนำคลาส ฟิลด์ในคลาสเหล่านี้ (และประเภทของคลาส) จะไม่ถูกกำหนดจนกว่าจะมีการเริ่มต้น—ซึ่งอาจไม่ใช่เมื่อคลาสสร้างอินสแตนซ์—และไม่ได้กำหนดประเภทการส่งคืนของฟิลด์และฟังก์ชัน เลยในสคีมา ยิ่งไปกว่านั้น โครงสร้างของคลาสสามารถเปลี่ยนแปลงได้อย่างง่ายดายในขณะรันไทม์—สามารถเพิ่มหรือลบฟิลด์ สามารถเปลี่ยนประเภทได้ ฯลฯ แม้ว่าจะเป็นไปได้โดยใช้การสะท้อนใน C# การสะท้อนกลับแสดงถึง “ศาสตร์มืด” ของภาษานั้น และ นักพัฒนาคาดว่าจะหยุดการทำงาน
ฉันถูกนำเสนอเกี่ยวกับปัญหานี้ในที่ทำงานเมื่อไม่กี่ปีที่ผ่านมาเมื่อทำงานในทีมหลักของ Toptal เรากำลังสร้างแดชบอร์ดที่คล่องตัวสำหรับทีมของเรา ซึ่งจำเป็นต้องรวดเร็ว มิฉะนั้น นักพัฒนาและเจ้าของผลิตภัณฑ์จะไม่ใช้มัน เราดึงข้อมูลจากแหล่งที่มาต่างๆ ได้แก่ ระบบติดตามงาน เครื่องมือการจัดการโครงการ และฐานข้อมูล ไซต์สร้างขึ้นใน Node.js/Express และเรามีแคชหน่วยความจำเพื่อลดการเรียกไปยังแหล่งข้อมูลเหล่านี้ อย่างไรก็ตาม กระบวนการพัฒนาแบบวนซ้ำที่รวดเร็วและรวดเร็วของเราทำให้เราปรับใช้ (และเริ่มต้นใหม่ด้วยเหตุนี้) หลายครั้งต่อวัน ทำให้แคชใช้งานไม่ได้และทำให้สูญเสียประโยชน์หลายประการ
วิธีแก้ปัญหาที่ชัดเจนคือแคชนอกกระบวนการ เช่น Redis อย่างไรก็ตาม หลังจากการค้นคว้า ฉันพบว่าไม่มีไลบรารีการทำให้เป็นอนุกรมที่ดีสำหรับ JavaScript เมธอด JSON.stringify/JSON.parse ในตัวจะส่งคืนข้อมูลประเภทอ็อบเจ็กต์ โดยสูญเสียฟังก์ชันใดๆ บนต้นแบบของคลาสดั้งเดิม ซึ่งหมายความว่าอ็อบเจ็กต์ดีซีเรียลไลซ์ไม่สามารถใช้ "แทนที่" ในแอปพลิเคชันของเราได้ ดังนั้นจึงต้องมีการปรับโครงสร้างใหม่อย่างมากเพื่อทำงานกับการออกแบบทางเลือก
ข้อกำหนดสำหรับห้องสมุด
เพื่อรองรับการทำให้เป็นอนุกรมและดีซีเรียลไลซ์เซชันของข้อมูลที่กำหนดเองใน JavaScript ด้วยการแสดงแทนค่าดีซีเรียลไลซ์และต้นฉบับที่ใช้แทนกันได้ เราจำเป็นต้องมีไลบรารีการทำให้เป็นอันดับที่มีคุณสมบัติดังต่อไปนี้:
- การแสดงแทนค่าดีซีเรียลไลซ์ต้องมีต้นแบบเดียวกัน (ฟังก์ชัน, ตัวรับ, ตัวตั้งค่า) กับอ็อบเจ็กต์ดั้งเดิม
- ไลบรารีควรสนับสนุนประเภทความซับซ้อนที่ซ้อนกัน (รวมถึงอาร์เรย์และแผนที่) โดยมีการตั้งค่าต้นแบบของวัตถุที่ซ้อนกันอย่างถูกต้อง
- ควรเป็นไปได้ที่จะทำให้เป็นอนุกรมและดีซีเรียลไลซ์ออบเจ็กต์เดียวกันหลายครั้ง—กระบวนการควรเป็นแบบไม่มีอำนาจ
- รูปแบบการทำให้เป็นอนุกรมควรสามารถส่งผ่าน TCP ได้อย่างง่ายดายและสามารถจัดเก็บได้โดยใช้ Redis หรือบริการที่คล้ายคลึงกัน
- ควรมีการเปลี่ยนแปลงโค้ดขั้นต่ำเพื่อทำเครื่องหมายคลาสเป็นแบบซีเรียลไลซ์ได้
- กิจวัตรของห้องสมุดควรจะรวดเร็ว
- ตามหลักการแล้ว ควรมีวิธีการสนับสนุนการดีซีเรียลไลเซชันของคลาสเวอร์ชันเก่า ผ่านการแมป/การกำหนดเวอร์ชันบางประเภท
การดำเนินการ
เพื่ออุดช่องว่างนี้ ฉันตัดสินใจเขียน Tanagra.js ซึ่งเป็นไลบรารีการทำให้เป็นอนุกรมเอนกประสงค์สำหรับ JavaScript ชื่อของห้องสมุดเป็นการอ้างถึงหนึ่งในตอนที่ฉันชอบใน Star Trek: The Next Generation ซึ่งลูกเรือของ Enterprise ต้องเรียนรู้ที่จะสื่อสารกับมนุษย์ต่างดาวลึกลับที่มีภาษาที่ไม่สามารถเข้าใจได้ ไลบรารีการทำให้เป็นอันดับนี้สนับสนุนรูปแบบข้อมูลทั่วไปเพื่อหลีกเลี่ยงปัญหาดังกล่าว
Tanagra.js ได้รับการออกแบบมาให้เรียบง่ายและน้ำหนักเบา และขณะนี้รองรับ Node.js (ยังไม่ได้รับการทดสอบในเบราว์เซอร์ แต่ในทางทฤษฎี ควรจะใช้งานได้) และคลาส ES6 (รวมถึง Maps) การใช้งานหลักรองรับ JSON และรุ่นทดลองรองรับ Google Protocol Buffers ไลบรารีต้องการเพียง JavaScript มาตรฐานเท่านั้น (ปัจจุบันทดสอบกับ ES6 และ Node.js) โดยไม่มีการพึ่งพาคุณลักษณะทดลอง Babel transpiling หรือ TypeScript
คลาส Serializable ถูกทำเครื่องหมายด้วยการเรียกเมธอดเมื่อคลาสถูกเอ็กซ์พอร์ต:
module.exports = serializable(Foo, myUniqueSerialisationKey)
เมธอดจะส่งคืนพรอกซีไปยังคลาส ซึ่งสกัดกั้นคอนสตรัคเตอร์และฉีดตัวระบุที่ไม่ซ้ำกัน (ถ้าไม่ได้ระบุ ค่าดีฟอลต์นี้จะเป็นชื่อคลาส) คีย์นี้ถูกจัดลำดับกับข้อมูลที่เหลือ และคลาสยังเปิดเผยคีย์นี้เป็นฟิลด์สแตติก หากคลาสมีประเภทที่ซ้อนกัน (เช่น สมาชิกที่มีประเภทที่ต้องการการทำให้เป็นอนุกรม) พวกเขาจะถูกระบุใน method-call ด้วย:
module.exports = serializable(Foo, [Bar, Baz], myUniqueSerialisationKey)
(ประเภทที่ซ้อนกันสำหรับเวอร์ชันก่อนหน้าของคลาสยังสามารถระบุได้ในลักษณะเดียวกัน ตัวอย่างเช่น หากคุณทำให้ Foo1 เป็นอนุกรม ก็จะสามารถดีซีเรียลไลซ์เป็น Foo2)
ในระหว่างการทำให้เป็นอนุกรม ไลบรารีจะสร้างแผนที่สากลของคีย์ไปยังคลาสซ้ำๆ และใช้สิ่งนี้ในระหว่างการดีซีเรียลไลซ์เซชัน (โปรดจำไว้ว่า คีย์จะถูกจัดลำดับกับข้อมูลที่เหลือ) เพื่อให้ทราบประเภทของคลาส "ระดับบนสุด" ไลบรารีต้องการให้ระบุสิ่งนี้ในการเรียกดีซีเรียลไลเซชัน:
const foo = decodeEntity(serializedFoo, Foo)
ไลบรารีการแมปอัตโนมัติแบบทดลองเดินแผนผังโมดูลและสร้างการแมปจากชื่อคลาส แต่สิ่งนี้ใช้ได้กับคลาสที่มีชื่อไม่ซ้ำกันเท่านั้น
เค้าโครงโครงการ
โครงการแบ่งออกเป็นหลายโมดูล:
- tanagra-core - ฟังก์ชันทั่วไปที่จำเป็นสำหรับรูปแบบการทำให้เป็นอนุกรมที่แตกต่างกัน รวมถึงฟังก์ชันสำหรับการทำเครื่องหมายคลาสเป็น serializable
- tanagra-json - ทำให้ข้อมูลเป็นอนุกรมในรูปแบบ JSON
- tanagra-protobuf - ทำให้ข้อมูลเป็นอนุกรมในรูปแบบ Google protobuffers (ทดลอง)
- tanagra-protobuf-redis-cache - ไลบรารีตัวช่วยสำหรับจัดเก็บ protobufs ที่ต่อเนื่องกันใน Redis
- tanagra-auto-mapper - เดินแผนผังโมดูลใน Node.js เพื่อสร้างแผนที่ของคลาส หมายความว่าผู้ใช้ไม่ต้องระบุประเภทที่จะทำการดีซีเรียลไลซ์ (ทดลอง)
โปรดทราบว่าห้องสมุดใช้การสะกดคำแบบสหรัฐอเมริกา

ตัวอย่างการใช้งาน
ตัวอย่างต่อไปนี้ประกาศคลาสที่ทำให้ซีเรียลไลซ์ได้และใช้โมดูล tanagra-json เพื่อทำให้เป็นอนุกรม/ดีซีเรียลไลซ์:
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)
ผลงาน
ฉันเปรียบเทียบประสิทธิภาพของตัวทำให้ซีเรียลไลเซอร์สองตัว (ตัวทำให้อนุกรม JSON และตัวซีเรียลไลเซอร์โปรโตบัฟรุ่นทดลอง) กับตัวควบคุม ( JSON.parse ดั้งเดิมและ JSON.stringify) ฉันทำการทดลองทั้งหมด 10 ครั้งโดยแต่ละครั้ง
ฉันทดสอบสิ่งนี้กับแล็ปท็อป Dell XPS15 ปี 2017 ที่มีหน่วยความจำ 32Gb ที่ใช้ Ubuntu 17.10
ฉันจัดลำดับวัตถุที่ซ้อนกันต่อไปนี้:
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 } } }, }
เขียนประสิทธิภาพ
วิธีการทำให้เป็นอนุกรม | Ave. อิงค์ การทดลองครั้งแรก (มิลลิวินาที) | เซนต์เดฟ อิงค์ การทดลองครั้งแรก (มิลลิวินาที) | Ave. อดีต การทดลองครั้งแรก (มิลลิวินาที) | เซนต์เดฟ อดีต. การทดลองครั้งแรก (มิลลิวินาที) |
JSON | 0.115 | 0.0903 | 0.0879 | 0.0256 |
Google Protobufs | 2.00 | 2.748 | 1.13 | 0.278 |
กลุ่มควบคุม | 0.0155 | 0.00726 | 0.0139 | 0.00570 |
อ่าน
วิธีการทำให้เป็นอนุกรม | Ave. อิงค์ การทดลองครั้งแรก (มิลลิวินาที) | เซนต์เดฟ อิงค์ การทดลองครั้งแรก (มิลลิวินาที) | Ave. อดีต การทดลองครั้งแรก (มิลลิวินาที) | เซนต์เดฟ อดีต. การทดลองครั้งแรก (มิลลิวินาที) |
JSON | 0.133 | 0.102 | 0.104 | 0.0429 |
Google Protobufs | 2.62 | 1.12 | 2.28 | 0.364 |
กลุ่มควบคุม | 0.0135 | 0.00729 | 0.0115 | 0.00390 |
สรุป
ตัวทำให้ซีเรียลไล เซอร์ JSON นั้นช้ากว่าการทำให้เป็นอนุกรมดั้งเดิมประมาณ 6-7 เท่า ตัวซีเรีย ลไลเซอร์ protobufs รุ่น ทดลองนั้นช้ากว่าตัวทำให้ซีเรียลไลเซอร์ JSON ประมาณ 13 เท่า หรือช้ากว่าการทำให้เป็นอนุกรมดั้งเดิม 100 เท่า
นอกจากนี้ การแคชภายในของสคีมา/ข้อมูลโครงสร้างภายในตัวสร้างซีเรียลไลเซอร์แต่ละตัวยังมีผลกระทบต่อประสิทธิภาพการทำงานอย่างชัดเจน สำหรับ JSON serializer การเขียนครั้งแรกช้ากว่าค่าเฉลี่ยประมาณสี่เท่า สำหรับ protobuf serializer จะช้ากว่าเก้าเท่า ดังนั้นการเขียนวัตถุที่แคชข้อมูลเมตาแล้วจึงเร็วกว่ามากในไลบรารีทั้งสอง
มีผลเช่นเดียวกันสำหรับการอ่าน สำหรับไลบรารี JSON การอ่านครั้งแรกจะช้ากว่าค่าเฉลี่ยประมาณสี่เท่า และสำหรับไลบรารี protobuf จะช้ากว่าประมาณสองเท่าครึ่ง
ปัญหาด้านประสิทธิภาพของ protobuf serializer หมายความว่ายังอยู่ในช่วงทดลอง และฉันอยากจะแนะนำก็ต่อเมื่อคุณต้องการรูปแบบด้วยเหตุผลบางประการเท่านั้น อย่างไรก็ตาม มันคุ้มค่าที่จะลงทุนบางเวลา เนื่องจากรูปแบบมีความชัดเจนกว่า JSON มาก ดังนั้นจึงดีกว่าสำหรับการส่งผ่านสาย Stack Exchange ใช้รูปแบบสำหรับการแคชภายใน
ตัวสร้างซีเรียลไลเซอร์ JSON นั้นมีประสิทธิภาพมากกว่าอย่างเห็นได้ชัด แต่ก็ยังช้ากว่าการใช้งานแบบเนทีฟอย่างมาก สำหรับแผนผังออบเจ็กต์ขนาดเล็ก ความแตกต่างนี้ไม่มีนัยสำคัญ (สองสามมิลลิวินาทีบนคำขอ 50ms จะไม่ทำลายประสิทธิภาพของไซต์ของคุณ) แต่นี่อาจกลายเป็นปัญหาสำหรับแผนผังออบเจ็กต์ขนาดใหญ่มาก และเป็นหนึ่งในลำดับความสำคัญในการพัฒนาของฉัน
แผนงาน
ห้องสมุดยังอยู่ในระยะเบต้า เครื่องซีเรียลไลเซอร์ JSON ได้รับการทดสอบอย่างดีและมีเสถียรภาพ นี่คือแผนงานในอีกไม่กี่เดือนข้างหน้า:
- การปรับปรุงประสิทธิภาพสำหรับทั้ง serializers
- รองรับ JavaScript ก่อน ES6 ได้ดีขึ้น
- รองรับมัณฑนากร ES-Next
ฉันไม่รู้จักไลบรารี JavaScript อื่นที่สนับสนุนการทำให้เป็นอนุกรมที่ซับซ้อน ข้อมูลออบเจ็กต์ที่ซ้อนกัน และการดีซีเรียลไลซ์เป็นประเภทดั้งเดิม หากคุณกำลังใช้ฟังก์ชันที่จะได้รับประโยชน์จากไลบรารี โปรดลองใช้ ติดต่อกับคำติชมของคุณ และพิจารณาร่วมให้ข้อมูล
หน้าแรกของโครงการ
ที่เก็บ GitHub