Replace lodash.cloneDeep with node.js structuredClone?

Hi, just been trying to do some optimisation of one of my nodes that currently uses RED.util.cloneMessage(msg) in the runtime.

A quick check of the Node-RED code seems to suggest that the cloneMessage function is using Lodash's cloneDeep function. But since we are now using node.js v18+ (v24+ for v5), it may be possible to use the native structuredClone method that was added in node.js v17.

There are a couple of difference that might need checking though?

  1. structuredClone Throws on a function input whereas cloneDeep copies a reference.
  2. structuredClone does not clone symbols whereas cloneDeep does.

I don't think either of those would affect node-red's usage but I'm not close enough to the code to know for sure.

What I do know is that structuredClone is meant to be significantly more performant - possibly 2-3x faster in fact since it is C++ native. Given how many messages can sometimes be cloned in Node-RED, I think this is significant and worth looking at.

2 Likes

I tried many object-cloning methods in my contrib nodes, e.g.(structuredClone(),loadsh.cloneDeep(), JSON.stringify/parse, RED.util.cloneMessage()),
each had some issues and scenarios it did not support, especially in UI (dashboard) nodes.

Eventually I implemented my own simple recursive method, which works flawlessly (though I did not do any performance monitoring).

function cloneObj(obj)
{
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    if (obj instanceof Date) {
        return new Date(obj);
    }
    if (obj instanceof Array) {
        return obj.map(item => cloneObj(item));
    }
    if (obj instanceof Function) {
		// return obj.bind({});
		return null;
    }
    const clonedObj = {};
	Object.keys(obj).forEach (key => {
		clonedObj[key] = cloneObj(obj[key]);
	});
	return clonedObj;
}

Not interested in that here, this is specifically about core runtime.

While this, of course, works, it is not going to be performant.

The whole reason for me looking at this is that I'm looking at maximum throughput. For example, if connecting to an MQTT broker that has a very large number of retained messages, we could get an influx of many 10's of thousands of messages. When that dump hits a node that needs to clone the messages, that's why I'm raising this because even a 10-20% performance improvement would be very impactful here.

This makes sense.

Note that you can also optimize your flows to clone messages only where needed.

node.send(msg, sendOptions) // sendOptions.cloneMessage = false