Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Fix #119 - Dashboard Render Performance #138

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions bugFix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Fixing Performance Bug on Dashboard Render

- Render times before any fixes: ~1900ms

The issue is within the blocks widget, which pulls down 10,000 blocks each time there's a new block (or on initial page view).

- Render time with 100 blocks fetched: ~160ms

Potential fixes:

- Virtual tables (a separate library used in conjunction with mui table)

- Test of this with 10,000 rows of light data renders in <20ms

- Reduce initial blocks fetched
- Skeleton components (which doesn't really solve the issue but provides a better UX)
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"nyc": {
"report-dir": "coverage-cypress"
},
"dependencies": {},
"dependencies": {
"react-virtualized": "^9.22.3"
},
"devDependencies": {
"@cypress/code-coverage": "^3.9.12",
"@emotion/react": "^11.6.0",
Expand All @@ -61,8 +63,10 @@
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^7.1.2",
"@types/node": "^18.11.9",
"@types/react": "^17.0.35",
"@types/react-router-dom": "^5.3.2",
"@types/react-virtualized": "^9.21.21",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"@vitejs/plugin-react": "^1.3.0",
Expand Down
233 changes: 233 additions & 0 deletions src/components/JUPVirtualizedTable/JUPVirtualizedTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import * as React from "react";
import clsx from "clsx";
import { Theme, styled } from "@mui/material/styles";
import TableCell from "@mui/material/TableCell";
import Paper from "@mui/material/Paper";
import { AutoSizer, Column, Table, TableCellRenderer, TableHeaderProps } from "react-virtualized";

const classes = {
flexContainer: "ReactVirtualizedDemo-flexContainer",
tableRow: "ReactVirtualizedDemo-tableRow",
tableRowHover: "ReactVirtualizedDemo-tableRowHover",
tableCell: "ReactVirtualizedDemo-tableCell",
noClick: "ReactVirtualizedDemo-noClick",
};

interface ColumnData {
dataKey: string;
label: string;
numeric?: boolean;
width: number;
}

interface Row {
index: number;
}

interface MuiVirtualizedTableProps {
columns: readonly ColumnData[];
headerHeight?: number;
onRowClick?: () => void;
rowCount: number;
rowGetter: (row: Row) => Data;
rowHeight?: number;
}

class MuiVirtualizedTable extends React.PureComponent<MuiVirtualizedTableProps> {
static defaultProps = {
headerHeight: 48,
rowHeight: 48,
};

getRowClassName = ({ index }: Row) => {
const { onRowClick } = this.props;

return clsx(classes.tableRow, classes.flexContainer, {
[classes.tableRowHover]: index !== -1 && onRowClick != null,
});
};

cellRenderer: TableCellRenderer = ({ cellData, columnIndex }) => {
const { columns, rowHeight, onRowClick } = this.props;
return (
<TableCell
component="div"
className={clsx(classes.tableCell, classes.flexContainer, {
[classes.noClick]: onRowClick == null,
})}
variant="body"
style={{ height: rowHeight }}
align={(columnIndex != null && columns[columnIndex].numeric) || false ? "right" : "left"}
>
{cellData}
</TableCell>
);
};

headerRenderer = ({ label, columnIndex }: TableHeaderProps & { columnIndex: number }) => {
const { headerHeight, columns } = this.props;

return (
<TableCell
component="div"
className={clsx(classes.tableCell, classes.flexContainer, classes.noClick)}
variant="head"
style={{ height: headerHeight }}
align={columns[columnIndex].numeric || false ? "right" : "left"}
>
<span>{label}</span>
</TableCell>
);
};

render() {
const { columns, rowHeight, headerHeight, ...tableProps } = this.props;
return (
<AutoSizer>
{({ height, width }) => (
<Table
height={height}
width={width}
rowHeight={rowHeight!}
gridStyle={{
direction: "inherit",
}}
headerHeight={headerHeight!}
{...tableProps}
rowClassName={this.getRowClassName}
>
{columns.map(({ dataKey, ...other }, index) => {
return (
<Column
key={dataKey}
headerRenderer={(headerProps) =>
this.headerRenderer({
...headerProps,
columnIndex: index,
})
}
className={classes.flexContainer}
cellRenderer={this.cellRenderer}
dataKey={dataKey}
{...other}
/>
);
})}
</Table>
)}
</AutoSizer>
);
}
}

// ---

interface Data {
calories: number;
carbs: number;
dessert: string;
fat: number;
id: number;
protein: number;
}
type Sample = [string, number, number, number, number];

const sample: readonly Sample[] = [
["Frozen yoghurt", 159, 6.0, 24, 4.0],
["Ice cream sandwich", 237, 9.0, 37, 4.3],
["Eclair", 262, 16.0, 24, 6.0],
["Cupcake", 305, 3.7, 67, 4.3],
["Gingerbread", 356, 16.0, 49, 3.9],
];

function createData(id: number, dessert: string, calories: number, fat: number, carbs: number, protein: number): Data {
return { id, dessert, calories, fat, carbs, protein };
}

const rows: Data[] = [];

for (let i = 0; i < 10000; i += 1) {
const randomSelection = sample[Math.floor(Math.random() * sample.length)];
rows.push(createData(i, ...randomSelection));
}

const JUPVirtualizedTable = () => {
return (
<Paper style={{ height: 400, width: "80%" }}>
<VirtualizedTable
rowCount={rows.length}
rowGetter={({ index }) => rows[index]}
columns={[
{
width: 200,
label: "Dessert",
dataKey: "dessert",
},
{
width: 120,
label: "Calories\u00A0(g)",
dataKey: "calories",
numeric: true,
},
{
width: 120,
label: "Fat\u00A0(g)",
dataKey: "fat",
numeric: true,
},
{
width: 120,
label: "Carbs\u00A0(g)",
dataKey: "carbs",
numeric: true,
},
{
width: 120,
label: "Protein\u00A0(g)",
dataKey: "protein",
numeric: true,
},
]}
/>
</Paper>
);
};

// Styles

const styles = ({ theme }: { theme: Theme }) =>
({
// temporary right-to-left patch, waiting for
// https://github.com/bvaughn/react-virtualized/issues/454
"& .ReactVirtualized__Table__headerRow": {
...(theme.direction === "rtl" && {
paddingLeft: "0 !important",
}),
...(theme.direction !== "rtl" && {
paddingRight: undefined,
}),
},
[`& .${classes.flexContainer}`]: {
display: "flex",
alignItems: "center",
boxSizing: "border-box",
},
[`& .${classes.tableRow}`]: {
cursor: "pointer",
},
[`& .${classes.tableRowHover}`]: {
"&:hover": {
backgroundColor: theme.palette.grey[200],
},
},
[`& .${classes.tableCell}`]: {
flex: 1,
},
[`& .${classes.noClick}`]: {
cursor: "initial",
},
} as const);

const VirtualizedTable = styled(MuiVirtualizedTable)(styles);

export default React.memo(JUPVirtualizedTable);
1 change: 1 addition & 0 deletions src/components/JUPVirtualizedTable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./JUPVirtualizedTable";
2 changes: 2 additions & 0 deletions src/views/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import BlocksWidget from "./components/Widgets/BlocksWidget";
import PortfolioWidget from "./components/Widgets/PortfolioWidget";
import DEXWidget from "./components/Widgets/DEXWidget";
import useBreakpoint from "hooks/useBreakpoint";
import VirtualizedList from "components/JUPVirtualizedTable";

const Dashboard: React.FC = () => {
const isMobileExtraLarge = useBreakpoint("<", "xl");
Expand Down Expand Up @@ -36,6 +37,7 @@ const Dashboard: React.FC = () => {
<BlocksWidget />
</Grid>
</Grid>
<VirtualizedList />
</Page>
);
};
Expand Down
21 changes: 21 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { resolve } from "path";
import tsconfigPaths from "vite-tsconfig-paths";
import checker from "vite-plugin-checker"; // error overlays when build issues arrise
import istanbul from "vite-plugin-istanbul";
import fs from "fs";
import path from "path";

export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd());
Expand Down Expand Up @@ -100,10 +102,29 @@ export default defineConfig(({ command, mode }) => {
extension: [".js", ".ts", ".tsx"],
requireEnv: mode === "development" ? false : true,
}),
reactVirtualized(),
],
build: {
outDir: "dist",
sourcemap: true, // istanbul reports need this for dev
},
};
});

// patches a problem with using react-virtualized came from:
// https://github.com/uber/baseweb/issues/4129
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;

const reactVirtualized = () => {
return {
name: "my:react-virtualized",
configResolved() {
const file = require
.resolve("react-virtualized")
.replace(path.join("dist", "commonjs", "index.js"), path.join("dist", "es", "WindowScroller", "utils", "onScroll.js"));
const code = fs.readFileSync(file, "utf-8");
const modified = code.replace(WRONG_CODE, "");
fs.writeFileSync(file, modified);
},
};
};
Loading