This blog content explores the performance cost of inline functions in a React application. Before we begin, you need to understand what inline function means in the context of a React application.
Tham khảo:
What is an inline function?
Simply put, an inline function is a function that is defined and passed down inside the render method of a React component.
Let's understand this with a basic example of what an inline function might look like in a React application:
export default class CounterApp extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0 };
}
render() {
return (
<div className="App">
<button
onClick={
() => {
this.setState({
count: this.state.count + 1 }
);
}
}
>COUNT ({
this.state.count}
)</button>
</div>
);
}
}
The onClick prop, in the example above, is being passed as an inline function that calls this.setState. The function is defined within the render method, often inline with JSX. In the context of React applications, this is a very popular and widely used pattern.
Let's begin by listing some common patterns and techniques where inline functions are used in a React application:
- Render prop: A component prop that expects a function as a value. This function must return a JSX element, hence the name. Render prop is a good candidate for inline functions.
render() {
return (
<ListView
items={
items}
render={
({
item }
) => (<div>{
item.label}
</div>)}
/>
);
}
- DOM event handlers: DOM event handlers often make a call to setState or invoke some effect in the React application such as sending data to an API server.
<button
onClick={
() => {
this.setState({
count: this.state.count + 1 }
);
}
}
>
COUNT ({
this.state.count}
)
</button>
- Custom function or event handlers passed to child: Oftentimes, a child component requires a custom event handler to be passed down as props. Inline function is usually used in this scenario.
<Button onTap={
() => {
this.nextPage();
}
}
>Next<Button>
Alternatives to inline function
- Bind in constructor:
One of the most common patterns is to define the function within the
class component and then bind context to the function in constructor. We
only need to bind the current context if we want to use this keyword inside the handler function.
export default class CounterApp extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0 };
this.increaseCount = this.increaseCount.bind(this);
}
increaseCount() {
this.setState({
count: this.state.count + 1 }
);
}
render() {
return (
<div className="App">
<button onClick={
this.increaseCount}
>COUNT ({
this.state.count}
)</button>
</div>
);
}
}
- Bind in render: Another common
pattern is to bind the context inline when the function is passed down.
Eventually, this gets repetitive and hence the first approach is more
popular.
render() {
return (
<div className="App">
<button onClick={
this.increaseCount.bind(this)}
>COUNT ({
this.state.count}
)</button>
</div>
);
}
- Define as public field:
increaseCount = () => {
this.setState({
count: this.state.count + 1 }
);
};
render() {
return (
<div className="App">
<button onClick={
this.increaseCount}
>
COUNT ({
this.state.count}
)
</button>
</div>
);
}
There are several other approaches that React dev community has come up with, like using a helper method to bind all functions automatically in the constructor.
After understanding inline
functions with its examples and also taking a look at a few
alternatives, let's see why inline functions are so popular and widely
used.
Why use inline function
Inline function definitions are right where they are invoked or passed down. This means inline functions are easier to write, especially when the body of the function is of a few instructions such as calling setState. This works well within loops as well.
For example, when rendering
a list and assigning a DOM event handler to each list item, passing
down an inline function feels much more intuitive. For the same reason,
inline functions also make code more organized and readable.
Inline
arrow functions preserve context that means developers can use this
without having to worry about current execution context or explicitly
bind a context to the function.
<Button onTap={
() => {
this.prevPage();
}
}
>Previous<Button>
Inline functions make value from parent scope available within the function definition. It results in more intuitive code and developers need to pass down fewer parameters. Let's understand this with an example.
render() {
const {
count }
= this.state;
return (
<div className="App">
<button
onClick={
() => {
this.setState({
count: count + 1 }
);
}
}
>
COUNT ({
count}
)
</button>
</div>
);
}
Here, the value of count is readily available to onClick event handlers. This behavior is called closing over.For
these reasons, React developers make use of inline functions heavily.
That said, inline function has also been a hot topic of debate because
of performance concerns. Let's take a look at a few of these arguments.
Arguments against inline functions
- A new function is defined every time the render method is called. It results in frequent garbage collection, and hence performance loss.
- There is an eslint config that advises against using inline function jsx-no-bind. The idea behind this rule is when an inline function is passed down to a child component, React uses reference checks to re-render the component. This can result in child component rendering again and again as a reference to the passed prop value i.e. inline function. In this case, it doesn't match the original one.
<listitem onclick="{()" ==""> console.log('click')}></listitem>
Suppose ListItem component implements shouldComponentUpdate method where it checks for onClick prop reference. Since inline functions are created every time a component re-renders, this means that the ListItem component will reference a new function every time, which points to a different location in memory. The comparison checks in shouldComponentUpdate and tells React to re-render ListItem even though the inline function's behavior doesn't change. This results in unnecessary DOM updates and eventually reduces the performance of applications.
Performance concerns revolving around the Function.prototype.bind methods: when not using arrow functions, the inline function being passed down must be bound to a context if using this keyword inside the function. The practice of calling .bind before passing down an inline function raises performance concerns, but it has been fixed. For older browsers, Function.prototype.bind can be supplemented with a polyfill for performance.
Now that we've summarized a few arguments in favor of inline functions and a few arguments against it, let's investigate and see how inline functions really perform.
render() {
return (
<div>
{
this.state.timeThen > this.state.timeNow ? (
<>
<button onClick={
() => {
/* some action */ }
}
/>
<button onClick={
() => {
/* another action */ }
}
/>
</>
) : (
<button onClick={
() => {
/* yet another action */ }
}
/>
)}
</div>
);
}
Pre-optimization can often lead to bad code. For instance, let's try to get rid of all the inline function definitions in the component above and move them to the constructor because of performance concerns.We'd then have to define 3 custom event handlers in the class definition and bind context to all three functions in the constructor.
export default class CounterApp extends React.Component {
constructor(props) {
super(props);
this.state = {
timeThen: ...,
timeNow: Date.now()
};
this.someAction = this.someAction.bind(this);
this.anotherAction = this.anotherAction.bind(this);
this.yetAnotherAction = this.yetAnotherAction.bind(this);
}
someAction() {
/* some action */ }
anotherAction() {
/* another action */ }
yetAnotherAction() {
/* yet another action */ }
render() {
return (<div>
{
this.state.timeThen > this.state.timeNow ? (
<>
<button onClick={
this.someAction}
/>
<button onClick={
this.anotherAction}
/>
</>
) : (
<button onClick={
this.yetAnotherAction}
/>
)}
</div>);
}
}
This would increase the initialization time of the component significantly as opposed to inline function declarations where only one or two functions are defined and used at a time based on the result of condition timeThen > timeNow.Concerns around render props: A render prop is a method that returns a React element and is used to share state among React components.
Render props are meant to be invoked on each render since they share state between parent components and enclosed React elements. Inline functions are a good candidate for use in render prop and won't cause any performance concern.
render() {
return (
<ListView
items={
items}
render={
({
item }
) => (<div>{
item.label}
</div>)}
/>
)
}
In my experience, inline render props can sometimes be harder to maintain especially when render prop returns a larger more complicated component in terms of code size. In such cases, breaking down the component further or having a separate method that gets passed down as render prop has worked well for me.
Concerns around PureComponents and shouldComponentUpdate(): Pure components and various implementations of shouldComponentUpdate both do a strict type comparison of props and state. These act as performance enhancers by letting React know when or when not to trigger a render based on changes to state and props. Since inline functions are created on every render, when such a method is passed down to a pure component or a component that implements the shouldComponentUpdate method, it can lead to an unnecessary render. This is because of the changed reference of the inline function.
To overcome this, consider skipping checks on all function props in shouldComponentUpdate(). This assumes that inline functions passed to the component are only different in reference and not behavior. If there is a difference in the behavior of the function passed down, it will result in a missed render and eventually lead to bugs in the component's state and effects.
Conclusion
A common rule of thumb is to measure performance of the app and only optimize if needed. Performance impact of inline function, often categorized under micro-optimizations, is always a tradeoff between code readability, performance gain, code organization, etc that must be thought out carefully on a case by case basis and pre-optimization should be avoided.
In this blog post, we observed that inline functions don't necessarily bring in a lot of performance cost. They are widely used because of ease of writing, reading and organizing inline functions, especially when inline function definitions are short and simple.
No comments:
Post a Comment