Skip to content

Replace the Serializer with node:v8 #171

Open

Description

@avivkeller

Currently, Webpack implements a custom serialization mechanism for its caching system. This requires maintaining complex serialization logic and keeping it updated as JavaScript features evolve.

Instead of relying on a custom serializer, we can delegate object encoding/decoding to Node.js's built-in v8.serialize and v8.deserialize.

For this approach to work, all Webpack-internal classes participating in the cache must define a stable object representation.

For a class to be Serializable:

  • It must implement an instance method:

    toObject() -> Object // Note: Classes _can_ be serialized directly, but they will be converted to Objects by V8

    Produces a plain object representation safe to pass into the serializer.

  • It must implement a static method:

    fromObject(obj: Object) -> Instance

    Reconstructs the class instance from the serialized representation.

For example,

class ModuleDependency {
  constructor(request, range) {
    this.request = request;
    this.range = range;
    this.optional = false;
    this.weak = false;
  }
  
  toObject() {
    return {
      request: this.request,
      range: this.range ? [this.range[0], this.range[1]] : null,
      optional: this.optional,
      weak: this.weak
    };
  }
  
  static fromObject(obj) {
    const dependency = new ModuleDependency(obj.request, obj.range);
    dependency.optional = obj.optional;
    dependency.weak = obj.weak;
    return dependency;
  }
}

Activity

  1. alexander-akait commented on Aug 15, 2025

    @alexander-akait
    Member

    Do you profile it deeply? Where do we waste the most time?

  2. avivkeller commented on Aug 15, 2025

    @avivkeller
    MemberAuthor
    Benchmark
    const ObjectMiddleware = require('./lib/serialization/ObjectMiddleware');
    const assert = require('node:assert');
    
    const middleware = new ObjectMiddleware(() => { });
    
    const datasets = {
    	small_object: () => ({
    		id: 1,
    		name: "test",
    		values: [1, 2, 3, 4, 5]
    	}),
    
    	small_array: () => Array(1000).fill(0).map((_, i) => ({
    		id: i,
    		value: Math.random()
    	})),
    
    	medium_nested: () => {
    		let obj = { value: 'base' };
    		for (let i = 0; i < 50; i++) {
    			obj = {
    				[`level_${i}`]: obj,
    				data: Array(20).fill(i),
    				metadata: { depth: i, timestamp: Date.now() }
    			};
    		}
    		return obj;
    	},
    
    	medium_wide: () => {
    		const obj = {};
    		for (let i = 0; i < 10000; i++) {
    			obj[`prop_${i}`] = {
    				id: i,
    				name: `item_${i}`,
    				values: Array(10).fill(0).map(() => Math.random()),
    				active: i % 2 === 0
    			};
    		}
    		return obj;
    	},
    
    	large_user_data: () => ({
    		users: Array(50000).fill(0).map((_, i) => ({
    			id: i,
    			username: `user_${i}`,
    			email: `user${i}@example.com`,
    			profile: {
    				firstName: `First${i}`,
    				lastName: `Last${i}`,
    				bio: "Lorem ipsum ".repeat(50),
    				preferences: {
    					theme: i % 2 === 0 ? 'dark' : 'light',
    					notifications: Array(10).fill(0).map((_, j) => ({
    						type: `notification_${j}`,
    						enabled: (i + j) % 2 === 0
    					}))
    				}
    			},
    			posts: Array(5).fill(0).map((_, j) => ({
    				id: `${i}_${j}`,
    				title: `Post ${j} by user ${i}`,
    				content: "Content ".repeat(100),
    				tags: [`tag${j}`, `category${i % 10}`],
    				comments: Array(3).fill(0).map((_, k) => ({
    					id: k,
    					author: `commenter_${(i + k) % 1000}`,
    					text: "Comment text ".repeat(20)
    				}))
    			}))
    		})),
    		metadata: {
    			totalUsers: 50000,
    			generated: new Date().toISOString(),
    			version: "1.0.0"
    		}
    	}),
    
    	large_analytics: () => ({
    		events: Array(100000).fill(0).map((_, i) => ({
    			id: i,
    			timestamp: Date.now() - (Math.random() * 30 * 24 * 60 * 60 * 1000),
    			userId: Math.floor(Math.random() * 10000),
    			eventType: ['click', 'view', 'purchase', 'signup', 'logout'][i % 5],
    			properties: {
    				page: `/page/${i % 100}`,
    				referrer: `https://example${i % 50}.com`,
    				userAgent: `Browser ${i % 10}`,
    				sessionId: `session_${Math.floor(i / 100)}`,
    				experiments: Array(5).fill(0).map((_, j) => ({
    					id: `exp_${j}`,
    					variant: ['A', 'B', 'C'][i % 3]
    				}))
    			},
    			customData: {
    				scores: Array(20).fill(0).map(() => Math.random() * 100),
    				flags: Array(10).fill(0).map(() => Math.random() > 0.5)
    			}
    		})),
    		aggregations: {
    			daily: Array(30).fill(0).map((_, i) => ({
    				date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString(),
    				metrics: {
    					pageViews: Math.floor(Math.random() * 10000),
    					uniqueUsers: Math.floor(Math.random() * 5000),
    					conversions: Math.floor(Math.random() * 100)
    				}
    			}))
    		}
    	}),
    
    	extreme_string: () => ({
    		megaString: "X".repeat(50 * 1024 * 1024),
    		data: "test"
    	}),
    
    	extreme_deep: () => {
    		let obj = { value: 'deepest', data: Array(100).fill(0).map(() => Math.random()) };
    		for (let i = 0; i < 500; i++) {
    			obj = {
    				[`level_${i}`]: obj,
    				siblings: Array(5).fill(0).map((_, j) => ({
    					id: `${i}_${j}`,
    					data: Array(50).fill(j)
    				}))
    			};
    		}
    		return obj;
    	},
    
    	extreme_wide: () => {
    		const obj = {};
    		for (let i = 0; i < 100000; i++) {
    			obj[`key_${i}`] = {
    				id: i,
    				timestamp: Date.now(),
    				payload: {
    					values: Array(100).fill(0).map(() => Math.random()),
    					metadata: {
    						type: `type_${i % 20}`,
    						category: `cat_${i % 5}`,
    						flags: [i % 2 === 0, i % 3 === 0, i % 5 === 0, i % 7 === 0]
    					},
    					nested: {
    						level1: { level2: { level3: { data: `deep_${i}` } } }
    					}
    				}
    			};
    		}
    		return obj;
    	},
    
    	mixed_types: () => {
    		return {
    			string: "Hello World",
    			number: 42,
    			bigNumber: Number.MAX_SAFE_INTEGER,
    			boolean: true,
    			nullValue: null,
    			undefinedValue: undefined,
    			nanValue: NaN,
    			infinityValue: Infinity,
    			negativeZero: -0,
    			date: new Date(),
    			regex: /test.*pattern/gi,
    			buffer: Buffer.from("Binary data"),
    			array: Array(10000).fill(0).map(() => ({
    				mixed: [Math.random(), `str_${Math.random()}`, Math.random() > 0.5]
    			})),
    			map: new Map([['key1', 'value1'], ['key2', 'value2']]),
    			set: new Set([1, 2, 3, 'string', true]),
    			symbol: Symbol("unique"),
    			proxy: new Proxy({ a: 1 }, {
    				get(target, prop) {
    					return prop in target ? target[prop] : `missing_${prop}`;
    				}
    			}),
    			sparseArray: [1, , , 4],
    		};
    	}
    };
    
    for (let i = 0; i < 5; i++) assert(middleware.serialize(Object.values(datasets).map(fn => fn()), { logger: console }));
    Image

    (.process on ObjectMiddleware is the slowest path)

    Image
  3. avivkeller commented on Aug 15, 2025

    @avivkeller
    MemberAuthor

    Serialization is only used for hashing, right? @ovflowd were trying to figure out where else it was used during our meeting

  4. avivkeller commented on Aug 16, 2025

    @avivkeller
    MemberAuthor

    I'm sure there's a dependency that might be able to cut down on this custom code (msgpack?)

  5. alexander-akait commented on Aug 17, 2025

    @alexander-akait
    Member

    Serialization is only used for hashing, right? @ovflowd were trying to figure out where else it was used during our meeting

    For caching

    I'm sure there's a dependency that might be able to cut down on this custom code (msgpack?)

    Maybe when we wrote this, there were no ready-made solutions at all

  6. changed the title [-]Speed Up (and Document) the Serializer[/-] [+]Replace the Serializer with `msgpack`[/+] on Aug 17, 2025
  7. changed the title [-]Replace the Serializer with `msgpack`[/-] [+]Replace the Serializer with dep[/+] on Aug 17, 2025
  8. avivkeller commented on Aug 17, 2025

    @avivkeller
    MemberAuthor

    It would definitely be nice to replace this with a dependency. Running the same benchmark with a few other libraries:

    • Current implementation: ~52 seconds
    • node:v8: ~38 seconds
    • cbor-x: ~44 seconds
  9. changed the title [-]Replace the Serializer with dep[/-] [+]Replace the Serializer with `node:v8`[/+] on Aug 17, 2025
  10. ovflowd commented on Aug 18, 2025

    @ovflowd
    Contributor

    Maybe when we wrote this, there were no ready-made solutions at all

    That seems an accurate statement lol

  11. alexander-akait commented on Nov 26, 2025

    @alexander-akait
    Member

    @avivkeller Just wonder, why you get toObject and fromObject names? Any reference for it? For me serialize and deserialize are more clear... I tried using v8, looks like we can implement it, but need refactor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions