This is the first article in a series looking at how to write performant Javascript code when it matters. When you are writing code you need to think about where it will be used and what the effects are. Working with small amounts of data, you can get away with many inefficiencies, but it's not long before the beautiful code you wrote bites you because it's plain nasty on the inside. When it matters is when you are processing lots of data or scripting the inside of a frequently executed loop. This series aims to help you spot and avoid costly mistakes in those circumstances.
Let's take immutability. It's become almost a mantra. Perhaps I should feel dirty for mutating an array? Let me explain why that is not always the case.
- Creating new objects allocates memory
- Allocating memory takes time
- Garbage collection takes time when you allocate - causing glitches
- Garbage collection takes time to get rid of the things you just allocated
You typically use immutability because it makes it easier to manage state that may be shared. It's a bit like using Typescript to make it easier to ensure you have the right variables isn't it? No it isn't. Typescript is gone by the time you run your code, those memory allocations are hitting your users time and again.
Now none of this matters if your arrays are 20 entries long and infrequently changing. Maybe you have places where that isn't the case, I know I do.
React states
Let's say we have an array in React we are going to use for something in a renderer. Stick the results in a virtual list maybe. Let's say the user can add things, other users can add things. Let's say this is a chat! Ok, so we can add things and the network can add things - let's pretend there's an event emitter for that.
function Chat() {
const [messages, setMessages] = useState([])
useEffect(()=>{
someEventEmitter.on("newMessage", addMessage);
return ()=>someEventEmitter.off("newMessage", addMessage);
}, [])
return <VirtualList items={messages}>
{message=><Message details={message}/>}
</VirtualList>
function addMessage(message) {
setMessages([...messages, message]);
}
}
Beautiful immutable messages. Woo. Mind you. How expensive is that?
Let's say you become suddenly popular - or you decide to take a live feed of stock prices or something - let's say you got 10,000 messages in there over some time. Let's say each message was roughly 140 characters long. Let's say it's utf8 and that's 140 bytes.
Have a guess how much memory you allocated? The final list is a whopping 1.4mb - but how much did you allocate along the way? Have a guess... The answer is 7GB. Were you close? 7GB. Can you imagine the glitching. But hey at least you managed to keep immutability - because phew, someone could have been using that list... Except they couldn't could they. That list was local. You could have kept an array in a ref and mutated it (see I said mutate again, X rated post!)
function Chat() {
const [, refresh] = useState(0)
const messages = useRef([])
useEffect(()=>{
someEventEmitter.on("newMessage", addMessage);
return ()=>someEventEmitter.off("newMessage", addMessage);
}, [])
return <VirtualList items={messages.current}>
{message=><Message details={message}/>
</VirtualList>
function addMessage(message) {
//Don't look mum
messages.current.push(message)
//Get the whole thing to render again
refresh(performance.now())
}
}
A small saving of 99.98% of the memory immutability cost us.
Conclusion
I'm not saying immutability is always bad. It clearly isn't. But it's frighteningly easy to get into a mess by using it incorrectly.
This example focused on memory, but performance is another issue.
How fast can you add 10,000 integers to an array using immutability?
85,000 times a second if you care to do it the fastest way, 26,000 times with a push
and errr.... 20 times with the spread operator. Just sayin'