memoize Item, clicks on mobile are fast now!
This was a surprisingly big win! Item is heavier than it looks, because it has like 6 Chakra components, which aren't expensive but aren't _cheap_ in a re-rendered list that needs to be fast, you know? And it's even more important on search, where there's a lot of items on the page. (we should virtualize it too but that's a thing for another day)
This commit is contained in:
parent
bf76065faf
commit
8c653ce879
6 changed files with 84 additions and 63 deletions
|
@ -29,12 +29,15 @@ const LoadableItemSupportDrawer = loadable(() =>
|
|||
* In fact, this component can't trigger wear or unwear events! When you click
|
||||
* it in the app, you're actually clicking a <label> that wraps the radio or
|
||||
* checkbox. We _do_ control the Remove button in here, though!
|
||||
*
|
||||
* NOTE: This component is memoized with React.memo. It's surpisingly expensive
|
||||
* to re-render, because Chakra components are a lil bit expensive from
|
||||
* their internal complexity, and we have a lot of them here. And it can
|
||||
* add up when there's a lot of Items in the list. This contributes to
|
||||
* wearing/unwearing items being noticeably slower on lower-power
|
||||
* devices.
|
||||
*/
|
||||
export function Item({ item, itemNameId, outfitState, dispatchToOutfit }) {
|
||||
const { wornItemIds, allItemIds } = outfitState;
|
||||
const isWorn = wornItemIds.includes(item.id);
|
||||
const isInOutfit = allItemIds.includes(item.id);
|
||||
|
||||
function Item({ item, itemNameId, isWorn, isInOutfit, dispatchToOutfit }) {
|
||||
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
|
@ -80,7 +83,7 @@ export function Item({ item, itemNameId, outfitState, dispatchToOutfit }) {
|
|||
<SupportOnly>
|
||||
<LoadableItemSupportDrawer
|
||||
item={item}
|
||||
outfitState={outfitState}
|
||||
outfitState="STOPSHIP"
|
||||
isOpen={supportDrawerIsOpen}
|
||||
onClose={() => setSupportDrawerIsOpen(false)}
|
||||
/>
|
||||
|
@ -290,3 +293,5 @@ export function ItemListSkeleton({ count }) {
|
|||
*/
|
||||
const containerHasFocus =
|
||||
".item-container:hover &, input:focus + .item-container &";
|
||||
|
||||
export default React.memo(Item);
|
||||
|
|
|
@ -14,7 +14,7 @@ import { EditIcon } from "@chakra-ui/icons";
|
|||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Delay, Heading1, Heading2 } from "../util";
|
||||
import { Item, ItemListContainer, ItemListSkeleton } from "./Item";
|
||||
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
|
||||
|
||||
/**
|
||||
* ItemsPanel shows the items in the current outfit, and lets the user toggle
|
||||
|
@ -120,7 +120,8 @@ function ItemZoneGroup({ zoneLabel, items, outfitState, dispatchToOutfit }) {
|
|||
<Item
|
||||
item={item}
|
||||
itemNameId={itemNameId}
|
||||
outfitState={outfitState}
|
||||
isWorn={outfitState.wornItemIds.includes(item.id)}
|
||||
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</label>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Box, Text, VisuallyHidden } from "@chakra-ui/core";
|
|||
import { useQuery } from "@apollo/client";
|
||||
|
||||
import { Delay, Heading1, useDebounce } from "../util";
|
||||
import { Item, ItemListContainer, ItemListSkeleton } from "./Item";
|
||||
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
|
||||
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
||||
|
||||
/**
|
||||
|
@ -173,7 +173,8 @@ function SearchResults({
|
|||
/>
|
||||
<Item
|
||||
item={item}
|
||||
outfitState={outfitState}
|
||||
isWorn={outfitState.wornItemIds.includes(item.id)}
|
||||
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</label>
|
||||
|
|
|
@ -4,7 +4,7 @@ import loadable from "@loadable/component";
|
|||
|
||||
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
|
||||
import OutfitPreview from "../components/OutfitPreview";
|
||||
import useOutfitState from "./useOutfitState.js";
|
||||
import useOutfitState, { OutfitStateContext } from "./useOutfitState.js";
|
||||
import { usePageTitle } from "../util";
|
||||
|
||||
const OutfitControls = loadable(() =>
|
||||
|
@ -44,58 +44,64 @@ function WardrobePage() {
|
|||
}, [error, toast]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
bottom="0"
|
||||
left="0"
|
||||
right="0"
|
||||
// Create a stacking context, so that our drawers and modals don't fight
|
||||
// with the z-indexes in here!
|
||||
zIndex="0"
|
||||
>
|
||||
<Grid
|
||||
templateAreas={{
|
||||
base: `"previewAndControls"
|
||||
"itemsAndSearch"`,
|
||||
lg: `"previewAndControls itemsAndSearch"`,
|
||||
}}
|
||||
templateRows={{
|
||||
base: "minmax(100px, 45%) minmax(300px, 55%)",
|
||||
lg: "100%",
|
||||
}}
|
||||
templateColumns={{
|
||||
base: "100%",
|
||||
lg: "50% 50%",
|
||||
}}
|
||||
height="100%"
|
||||
width="100%"
|
||||
// NOTE: Most components pass around outfitState directly, to make the data
|
||||
// relationships more explicit... but there are some deep components
|
||||
// that need it, where it's more useful and more performant to access
|
||||
// via context.
|
||||
<OutfitStateContext.Provider value={outfitState}>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
bottom="0"
|
||||
left="0"
|
||||
right="0"
|
||||
// Create a stacking context, so that our drawers and modals don't fight
|
||||
// with the z-indexes in here!
|
||||
zIndex="0"
|
||||
>
|
||||
<Box gridArea="previewAndControls" bg="gray.900" pos="relative">
|
||||
<Box position="absolute" top="0" bottom="0" left="0" right="0">
|
||||
<OutfitPreview
|
||||
speciesId={outfitState.speciesId}
|
||||
colorId={outfitState.colorId}
|
||||
pose={outfitState.pose}
|
||||
wornItemIds={outfitState.wornItemIds}
|
||||
/>
|
||||
<Grid
|
||||
templateAreas={{
|
||||
base: `"previewAndControls"
|
||||
"itemsAndSearch"`,
|
||||
lg: `"previewAndControls itemsAndSearch"`,
|
||||
}}
|
||||
templateRows={{
|
||||
base: "minmax(100px, 45%) minmax(300px, 55%)",
|
||||
lg: "100%",
|
||||
}}
|
||||
templateColumns={{
|
||||
base: "100%",
|
||||
lg: "50% 50%",
|
||||
}}
|
||||
height="100%"
|
||||
width="100%"
|
||||
>
|
||||
<Box gridArea="previewAndControls" bg="gray.900" pos="relative">
|
||||
<Box position="absolute" top="0" bottom="0" left="0" right="0">
|
||||
<OutfitPreview
|
||||
speciesId={outfitState.speciesId}
|
||||
colorId={outfitState.colorId}
|
||||
pose={outfitState.pose}
|
||||
wornItemIds={outfitState.wornItemIds}
|
||||
/>
|
||||
</Box>
|
||||
<Box position="absolute" top="0" bottom="0" left="0" right="0">
|
||||
<OutfitControls
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box position="absolute" top="0" bottom="0" left="0" right="0">
|
||||
<OutfitControls
|
||||
<Box gridArea="itemsAndSearch">
|
||||
<ItemsAndSearchPanels
|
||||
loading={loading}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box gridArea="itemsAndSearch">
|
||||
<ItemsAndSearchPanels
|
||||
loading={loading}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Box>
|
||||
</OutfitStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import { CheckCircleIcon, EditIcon, ExternalLinkIcon } from "@chakra-ui/icons";
|
|||
import ItemLayerSupportModal from "./ItemLayerSupportModal";
|
||||
import { OutfitLayers } from "../../components/OutfitPreview";
|
||||
import useOutfitAppearance from "../../components/useOutfitAppearance";
|
||||
import { OutfitStateContext } from "../useOutfitState";
|
||||
import useSupportSecret from "./useSupportSecret";
|
||||
|
||||
/**
|
||||
|
@ -36,7 +37,7 @@ import useSupportSecret from "./useSupportSecret";
|
|||
* This component controls the drawer element. The actual content is imported
|
||||
* from another lazy-loaded component!
|
||||
*/
|
||||
function ItemSupportDrawer({ item, outfitState, isOpen, onClose }) {
|
||||
function ItemSupportDrawer({ item, isOpen, onClose }) {
|
||||
const placement = useBreakpointValue({
|
||||
base: "bottom",
|
||||
lg: "right",
|
||||
|
@ -75,10 +76,7 @@ function ItemSupportDrawer({ item, outfitState, isOpen, onClose }) {
|
|||
<Box paddingBottom="5">
|
||||
<Stack spacing="8">
|
||||
<ItemSupportSpecialColorFields item={item} />
|
||||
<ItemSupportAppearanceFields
|
||||
item={item}
|
||||
outfitState={outfitState}
|
||||
/>
|
||||
<ItemSupportAppearanceFields item={item} />
|
||||
</Stack>
|
||||
</Box>
|
||||
</DrawerBody>
|
||||
|
@ -231,7 +229,15 @@ function ItemSupportSpecialColorFields({ item }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ItemSupportAppearanceFields({ item, outfitState }) {
|
||||
/**
|
||||
* NOTE: This component takes `outfitState` from context, rather than as a prop
|
||||
* from its parent, for performance reasons. We want `Item` to memoize
|
||||
* and generally skip re-rendering on `outfitState` changes, and to make
|
||||
* sure the context isn't accessed when the drawer is closed. So we use
|
||||
* it here, only when the drawer is open!
|
||||
*/
|
||||
function ItemSupportAppearanceFields({ item }) {
|
||||
const outfitState = React.useContext(OutfitStateContext);
|
||||
const { speciesId, colorId, pose } = outfitState;
|
||||
const { error, visibleLayers } = useOutfitAppearance({
|
||||
speciesId,
|
||||
|
|
|
@ -7,6 +7,8 @@ import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
|||
|
||||
enableMapSet();
|
||||
|
||||
export const OutfitStateContext = React.createContext(null);
|
||||
|
||||
function useOutfitState() {
|
||||
const apolloClient = useApolloClient();
|
||||
const initialState = parseOutfitUrl();
|
||||
|
|
Loading…
Reference in a new issue