-
Notifications
You must be signed in to change notification settings - Fork 0
Frontend Performance Issues #101
Comments
@dhallett and @seth both recommended that this may be caused by the table needing to do expensive calculations based on the CSS box properties like width per cell and could be reduced by one or both of the following:
|
This might be addressed by using MUI's Drawer instead of boxes as mentioned in #61 |
Usage of the MUI Drawer is still a viable and likely desireable play but virtualizing the ItemTable will likely have the largest effect here. More research of React virtual DOM support with MUI Table is needed and efficient tables is needed. |
This item will now be used to address performance related issues. |
Optimizing Performance for ReactThe DOMWeb pages are not just the HTML we see in the source code. When we write HTML, it is parsed by the browser to create a collection of nodes with mutable properties to be programatically interacted with called the Document Object Model or DOM. When we load a web page, the following happens:
The Virtual DOMManipulating the DOM is an expensive operation meaning it costs lots of processing power and/or time. To avoid manipulating the DOM, React maintains a separate, in-memory repreentation of the DOM called the Virtual DOM. With the Virtual DOM, changes to the DOM can be efficiently staged and compared with the existing DOM so that only necessary changes to the actual DOM are made keeping things fast. Wasted Renders and Expensive OperationsReact components are just JavaScript functions that render HTML which in turn is transformed into nodes within the DOM. This process is referred to as rendering. By default, React will re-render every component every time anything changes. This is appropriate when the value of props change but undesirable when the value of props stays the same. When a component is re-rendered but the value of its props stays the same its called a wasted render. Wasted renders are to be avoided in most cases because a wasted render results in unneccesary work to modify the DOM. Regular functions can also be expensive. We equally want to avoid re-running expensive functions if the inputs don't change. In general, its a bad idea to re-run a function with the same inputs to produce an output we already have. It would be faster just to use the output we already have. MemoizationMemoization is an optimization technique for improving performance with expensive functions. If the inputs to a function are the same as the last time the function was run, then the last result is returned instead of re-running the function. In React, there are three different typoes of memoization:
Memoizing a ComponentBoth React.PureComponent and React.memo can be used to memoize a component and help with preventing wasted renders. Because our project primarily uses function components, I will focus on React.memo. React.memo is a high order component meaning it takes a compenent as one of its arugments and returns a component. To use React.memo we simply wrap our function components in React.memo: // Declare Component to be Memoize
const PrintArrayComp = ({ array }) => array.map(
(item, index) => <p>{`[${index}] ${item}`}</p>
);
// Memoize Component
const PrintArray = React.memo(PrintArrayComp);
// Use Component
<PrintArray array={["apple", "orange", "banana"]} /> By default, React.memo will compare the old props to the new props using referential equality as opposed to value equality. We can override this behavior if we'd like by passing a second argument to React.memo containing a function that accepts the old and new props and returns a boolean value to say whether or not the props should be considered equal. Using our PrintArray component above, we can override the default behavior to consider props equal if the array is the same length: // Declare Component to be Memoize
const PrintArrayComp = ({ array }) => array.map(
(item, index) => <p>{`[${index}] ${item}`}</p>
);
// Declare Prop Comparison Override Function
const propsAreEqual = (prevProps, nextProps) => prevProps.length === nextProps.length
// Memoize Component
const PrintArray = React.memo(PrintArrayComp, propsAreEqual);
// Use Component
<PrintArray array={["apple", "orange", "banana"]} /> Read Pure Functional Components in React for more details. Memoizing PropsIn most cases, the default referential equality comparsion of React.memo is what we want but we need to ensure referential equality for this to work. To do so we have to separate our props into two groups:
Primitive data does not need to be memoized. Memoizing Non-Primitive DataWe can use React.useMemo to memoize non-primitive data bypassing a function that generates the data (a factory function) as the first argument and an array of values that should be watched for change before re-running our factory function (a dependency list) as the second argument: // Declare memoized component to print an object as a table
const ObjectToTable = React.memo(({object}) => (
object.keys.map( (key) => {
let value = object.key;
return <b>{key}</b>: {value};
})
));
// Memoize data to pass to component
const objectFromAPI = React.useMemo(
(apiParams) => getObject(apiParams),
[apiParams]
);
// Use memoized data with component
<ObjectToTable object={objectFromAPI} /> Memoizing FunctionsWe can use React.useCallback to memoize a function. The syntax is imilar to React.useMemo where we pass an argument as the first argument and a dependency list as the second argument but it differs in that React.useMemo returns the result of a function and React.useCallback returns the function itself to be run later. // Declare component that accepts an onClick event handler
const AlertOnClick = React.memo( ({clickHandler}) => (
<p onClick={clickHandler}>Click me!</p>
));
// Declare alert message
// This does not need memoized because it is primitive data
const alertMessage = "I've been clicked!";
// Memoize the callback
const clickHandler = React.useCallback( _ => alert(alertMessage), [alertMessage]);
// Use memoized callback with component
<AlertOnClick clickHandler={clickHandler}> For a more in depth look at React.memo, React.useMemo and React.useEffect see Introduction to React.memo, useMemo and useCallback. Optimization Always Comes At A CostOptimization is not magic, it is a tradeoff and should be used only when appropriate. For a detailed look at when and when not to use React.useMemo and React.useCallback, read When to useMemo and useCallback. Further Reading
|
Performance issues will likely need to be addressed by refactoring existing components with composable designs by utilizing the React |
One of the proposed solutions to improving performance is to implement virtualization in the ItemTable component. Based on some research the two most commonly used libraries for this are What do they do?Both of these libraries are used to implement windowing into a project. Windowing or virtualization improves the performance of a long list (in our case this is the rows of the table) by only writing what the user can see into the DOM. Currently, the How do they differ?According to the author
Because of this
|
Starting work on virtualizing the ItemTable |
A rough version of virtualization has been implemented in the ItemTable component. This comment will detail how it was implemented. Virtualization LibraryI decided to use Implementing VirtualizationTo implement virtualization the the children of the <TableBody {...getTableBodyProps()}>
// Map over the array of rows (items)
{rows.map((row) => {
prepareRow(row);
let isSelected = selectedRow.queue === row.original.queue && selectedRow.number === row.original.number
return (
<TableRow
hover
onClick={() => {
history.push(`/${row.original.queue}/${row.original.number}`);
setSelectedRow({ queue: row.original.queue, number: row.original.number });
}}
// This functionality should be achieved by using the selected prop and
// overriding the selected class but this applied the secondary color at 0.08% opacity.
// Overridding the root class is a workaround.
classes={{
root: (isSelected && rowCanBeSelected) ? classes.rowSelected : classes.bandedRows,
hover: classes.hoverBackgroundColor
}}
{...row.getRowProps()}
>
//Map over the array of cells for each item
{row.cells.map(cell => (
cell.render(_ => {
switch (cell.column.id) {
case "dateReceived":
return (
<ItemTableCell TableCellProps={cell.getCellProps()}>
<RelativeTime value={cell.value} />
</ItemTableCell>
);
case "lastUpdated":
return (
<LastUpdatedCell
time={cell.value}
ItemTableCellProps={cell.getCellProps()}
/>
);
default:
return (
<ItemTableCell TableCellProps={cell.getCellProps()}>
{cell.value}
</ItemTableCell>
);
}
})
))}
</TableRow>
);
})}
</TableBody> The only thing that needed to be added for virtualization was wrapping the rows in the The
It also provides two methods for us to use:
I didn't use either of the provided methods but these may be useful in the future as well. I also implemented memorized list item as shown in the docs for <TableBody {...getTableBodyProps()}>
+ <FixedSizeList
+ height={800}
+ itemCount={rows.length}
+ itemSize={120}
+ width="100%"
+ >
+ {memo(({ index, style }) => {
+ const row = rows[index]
prepareRow(row)
let isSelected = selectedRow.queue === row.original.queue && selectedRow.number === row.original.number
return (
<TableRow
style={style}
hover
onClick={() => {
history.push(`/${row.original.queue}/${row.original.number}`);
setSelectedRow({ queue: row.original.queue, number: row.original.number });
}}
// This functionality should be achieved by using the selected prop and
// overriding the selected class but this applied the secondary color at 0.08% opacity.
// Overridding the root class is a workaround.
classes={{
root: (isSelected && rowCanBeSelected) ? classes.rowSelected : classes.bandedRows,
hover: classes.hoverBackgroundColor
}}
{...row.getRowProps({ style, })}
>
{row.cells.map(cell => (
cell.render(_ => {
switch (cell.column.id) {
case "dateReceived":
return (
<ItemTableCell TableCellProps={cell.getCellProps()}>
<RelativeTime value={cell.value} />
</ItemTableCell>
);
case "lastUpdated":
return (
<LastUpdatedCell
time={cell.value}
ItemTableCellProps={cell.getCellProps()}
/>
);
default:
return (
<ItemTableCell TableCellProps={cell.getCellProps()}>
{cell.value}
</ItemTableCell>
);
}
})
))}
</TableRow>
);
+ }, areEqual,
+ [prepareRow, rows]
+ )}
+ </FixedSizeList>
</TableBody> So far this has provided a good performance boost but it will still need some tweaks as this change will impact the UI. |
Currently, the
Because this takes the scrollbar into account when calculating the width I was able to stop the scrollbar from rendering when it wasn't necessary. I tried to use this same principle to stop the extra vertical scrollbar from rendering. but this causes the entire page's vertical scroll to be controlled with the same scrollbar instead of just the table being scrollable. The |
Working on this. |
After finding a reply to this issue here I was able to make progress on finding a solution to the multiple scrollbar problem. To summarize the comment and the docs it references:
We should be able to render the table using these while keeping all of the components present. The next step is to find the proper implementation of these suggested style changes and see if they cause the table to render in the expected fashion. |
Here is a tutorial on how I implemented the Implementing the
|
After some help from @seth we were able to apply the styles we wanted to the actual This is also is caused by how
So because the |
Working on this. |
As mentioned in this comment from #133. The ItemTable re-render every time an item is selected. This happens because the Hooks for the ItemTable change as well as the
While this is an issue now the author of The |
This comment will be a more clear explanation of virtualization and its impact on performance. What slows down a React app?There are two many places where performance is impacted within a React app, the browser and React itself. Here are some of the specific issues that can cause performance issues for each area. Browser
React
What is virtualization/windowing?Virtualization means that the app only creates DOM elements that the user can see. A good example of how this works can be seen with Kindles or the Kindle app. When reading books through kindle the entire book's information is there but instead of using resources to render the whole book it instead renders just one page. How does
|
The current PR for this is workable but un-maintainable.
So far, some helpful resources that I have come across are:
For now, our code is ugly but functional. This is now the core of our issues with the frontend and needs to be addressed. We are not release candidate ready let alone v1. |
There are noticeable stutters on high end machines when opening and closing the sidebar. This is likely due to CSS transitions for width being overly CPU intensive. A more performant solution should be researched.
The text was updated successfully, but these errors were encountered: