React provides a superb developer experience: you define states and props, combine components in a way you want — and everything magically updates yet stays consistent. But... What memory effects hide beneath this nice-looking code? Let's see!
1. Class members: functions vs arrow functions
Here are two very similar classes. What's the difference?
class A {
x() {
console.log('Hi!')
}
}
class B {
y = () => console.log('Hi!')
}
Okay, okay you are right, y
is this
-bound 😉 But I wouldn't disturb you with such a trivial thing. There's an interesting memory implication I suggest you to spot.
⏳
⏳
⌛️
A.x
resides on A
prototype, and B.y
copy resides on each B
instance, meaning B
instances consume more memory.
Writing the same using only functions makes this more prominent:
function A() {
}
A.prototype.x = function() {
console.log('Hi!')
}
function B() {
this.y = () => console.log('Hi!')
}
A
instances are completely empty!
Why is it important?
When implementing React class components we often need this
-bound functions, and one possible option is an arrow function. In the following example each Button
instance has its own handleClick
member:
class Button {
constructor(props) {
this.props = props
}
render() {
return <button onClick={this.handleClick} />
}
handleClick = () => console.log(this.props.message)
}
Is it a problem?
In 99% of cases it's not — an arrow function instance is not that big. Just make sure you don't use it unless you need it. For example, if handleClick
calls some other class method, it's better be defined as a simple function:
class Button {
// ...
handleClick = () => this.logMessage()
logMessage() {
console.log(this.props.message)
}
}
2. Inner functions
What will the following code print? Or, in other words, is inner function referentially the same at each run?
function outer() {
function inner() {
console.log('Hi!')
}
return inner
}
console.log(outer() === outer())
⏳
⏳
⌛️
The inner function is referentially different at each run, and the code outputs false
.
Why is it important?
Inner functions are the common way to define handlers in React functional components:
function Button({message}) {
function handleClick() {
console.log(message)
}
return <button onClick={handleClick} />
}
In this example a new handleClick
is created on each function run, i.e. on each component render.
Someone told me useCallback
can fix this
function Button({message}) {
const handleClick = useCallback(function(m) {
console.log(m)
}, [message])
return <button onClick={handleClick} />
}
Now inner function(m)
is created only when message
changes, isn't it?
⏳
⏳
⌛️
No, useCallback
can't override how JavaScript works, and function(m)
is created at each component render.
Is it a problem?
Just like in previous example, it's fine in 99% of cases. However, if your handler doesn't need a closure over locals, you may define it outside the component:
function Button() {
return <button onClick={handleClick} />
}
function handleClick() {
console.log('Hi!')
}
Further reading
Official explanation on hooks performance
Thanks for reading this. Do you know other JavaScript memory concerns useful to keep in mind?
Top comments (0)