Skip to content

Commit

Permalink
feat: portfolio performance improvements (#1746)
Browse files Browse the repository at this point in the history
* fix: hook deps

* fix: portfolio container performance

* feat: minor performance optimizations

* feat: virtualized rows on assets table

* feat: context for usePortfolioNavigation

* feat: no count up on asset details (because of virtualization)

* fix: glitch when changing account on portfolio

* wip: tanstack virtual

* feat: virtualized asset tables

* chore: cleanup

* feat: virtualize rows in token picker

* fix: wrap dashboard tx history in portfolio container

* fix: bool checks

* fix: virtualise tx history rows

* fix: sidebar must be wrapped in PortfolioContainer to access selected account

* chore: export style

* feat: add countups back
  • Loading branch information
0xKheops authored Dec 16, 2024
1 parent db4e053 commit eb959d4
Show file tree
Hide file tree
Showing 20 changed files with 620 additions and 1,056 deletions.
1 change: 1 addition & 0 deletions apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@talismn/token-rates": "workspace:*",
"@talismn/util": "workspace:*",
"@tanstack/react-query": "5.59.16",
"@tanstack/react-virtual": "^3.11.1",
"@types/blueimp-md5": "2.18.2",
"@types/chrome": "0.0.279",
"@types/color": "3.0.6",
Expand Down
18 changes: 17 additions & 1 deletion apps/extension/src/@talisman/components/ScrollContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { classNames } from "@talismn/util"
import { forwardRef, RefObject, useEffect, useMemo, useRef, useState } from "react"

import { provideContext } from "@talisman/util/provideContext"

type ScrollContainerProps = {
className?: string
children?: React.ReactNode
Expand Down Expand Up @@ -67,7 +69,7 @@ export const ScrollContainer = forwardRef<HTMLDivElement, ScrollContainerProps>(
innerClassName,
)}
>
{children}
<ScrollContainerProvider refContainer={refDiv}>{children}</ScrollContainerProvider>
</div>
<div
className={classNames(
Expand All @@ -86,3 +88,17 @@ export const ScrollContainer = forwardRef<HTMLDivElement, ScrollContainerProps>(
},
)
ScrollContainer.displayName = "ScrollContainer"

const useScrollContainerProvider = ({
refContainer,
}: {
refContainer: RefObject<HTMLDivElement>
}) => {
return refContainer
}

const [ScrollContainerProvider, useScrollContainer] = provideContext(useScrollContainerProvider)

// this hook will provite a way for its children to access the ref of the scrollable element
// mainly useful when using a virtualizer or other scroll related libraries
export { useScrollContainer }
11 changes: 6 additions & 5 deletions apps/extension/src/ui/apps/dashboard/routes/Portfolio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ const BuyTokensOpener = () => {
}

export const PortfolioRoutes = () => (
<DashboardLayout sidebar="accounts">
<BuyTokensOpener />
<PortfolioContainer>
<PortfolioContainer>
<DashboardLayout sidebar="accounts">
<BuyTokensOpener />

{/* share layout to prevent tabs flickering */}
<PortfolioLayout toolbar={<PortfolioToolbar />}>
<Routes>
Expand All @@ -44,8 +45,8 @@ export const PortfolioRoutes = () => (
<Route path="*" element={<NavigateWithQuery url="tokens" />} />
</Routes>
</PortfolioLayout>
</PortfolioContainer>
</DashboardLayout>
</DashboardLayout>
</PortfolioContainer>
)

const PortfolioToolbar = () => (
Expand Down
23 changes: 13 additions & 10 deletions apps/extension/src/ui/apps/dashboard/routes/TxHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"
import { useOpenClose } from "talisman-ui"

import { ChainLogo } from "@ui/domains/Asset/ChainLogo"
import { PortfolioContainer } from "@ui/domains/Portfolio/PortfolioContainer"
import { usePortfolioNavigation } from "@ui/domains/Portfolio/usePortfolioNavigation"
import { TxHistoryList, TxHistoryProvider } from "@ui/domains/Transactions/TxHistory"
import { useTxHistory } from "@ui/domains/Transactions/TxHistory/TxHistoryContext"
Expand Down Expand Up @@ -86,15 +87,17 @@ const TxHistoryAccountFilter = () => {

export const TxHistory = () => {
return (
<DashboardLayout sidebar="accounts">
<TxHistoryProvider>
<TxHistoryAccountFilter />
<div className="min-w-[60rem]">
<Header />
<div className="h-8"></div>
<TxHistoryList />
</div>
</TxHistoryProvider>
</DashboardLayout>
<PortfolioContainer>
<DashboardLayout sidebar="accounts">
<TxHistoryProvider>
<TxHistoryAccountFilter />
<div className="min-w-[60rem]">
<Header />
<div className="h-8"></div>
<TxHistoryList />
</div>
</TxHistoryProvider>
</DashboardLayout>
</PortfolioContainer>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const PortfolioAssetsHeader: FC<{ backBtnTo?: string }> = ({ backBtnTo })
balancesByAddress.get(balance.address)?.push(balance)
})
return balancesByAddress
}, [allBalances.each])
}, [allBalances])

const balances = useMemo(
() =>
Expand Down
196 changes: 123 additions & 73 deletions apps/extension/src/ui/domains/Asset/TokenPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { Balances } from "@talismn/balances"
import { Token, TokenId } from "@talismn/chaindata-provider"
import { CheckCircleIcon } from "@talismn/icons"
import { classNames, planckToTokens } from "@talismn/util"
import { useVirtualizer } from "@tanstack/react-virtual"
import sortBy from "lodash/sortBy"
import { FC, useCallback, useDeferredValue, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useIntersection } from "react-use"

import { Address } from "@extension/core"
import { ScrollContainer } from "@talisman/components/ScrollContainer"
import { ScrollContainer, useScrollContainer } from "@talisman/components/ScrollContainer"
import { SearchInput } from "@talisman/components/SearchInput"
import {
useAccountByAddress,
Expand Down Expand Up @@ -67,6 +67,74 @@ const TokenRowSkeleton = () => (
</div>
)

type TokenData = {
id: string
token: Token
balances: Balances
chainNameSearch: string | null | undefined
chainName: string
chainLogo: string | null | undefined
hasFiatRate: boolean
}

const TokenRows: FC<{
tokens: TokenData[]
selectedTokenId?: TokenId
allowUntransferable?: boolean
onTokenClick: (tokenId: TokenId) => void
}> = ({ tokens, selectedTokenId, allowUntransferable, onTokenClick }) => {
const refContainer = useScrollContainer()
const ref = useRef<HTMLDivElement>(null)

const virtualizer = useVirtualizer({
count: tokens.length,
estimateSize: () => 58,
overscan: 5,
getScrollElement: () => refContainer.current,
})

if (!tokens.length) return null

return (
<div ref={ref}>
<div
className="relative w-full"
style={{
height: `${virtualizer.getTotalSize()}px`,
}}
>
{virtualizer.getVirtualItems().map((item) => {
const tokenData = tokens[item.index]
if (!tokenData) return null

return (
<div
key={item.key}
className="absolute left-0 top-0 w-full"
style={{
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}}
>
<TokenRow
key={item.key}
selected={tokenData.token.id === selectedTokenId}
token={tokenData.token}
balances={tokenData.balances}
chainName={tokenData.chainName}
chainLogo={tokenData.chainLogo}
hasFiatRate={tokenData.hasFiatRate}
allowUntransferable={allowUntransferable}
onClick={() => onTokenClick(tokenData.token.id)}
/>
</div>
)
})}
</div>
</div>
)
}

const TokenRow: FC<TokenRowProps> = ({
token,
selected,
Expand All @@ -84,23 +152,15 @@ const TokenRow: FC<TokenRowProps> = ({
tokensTotal: planckToTokens(planck.toString(), token.decimals),
isLoading: balances.each.find((b) => b.status === "cache"),
}
}, [balances.each, token.decimals])
}, [balances, token.decimals])

const isTransferable = useMemo(() => isTransferableToken(token), [token])

// there are more than 250 tokens so we should render only visible tokens to prevent performance issues
const refButton = useRef<HTMLButtonElement>(null)
const intersection = useIntersection(refButton, {
root: null,
rootMargin: "1000px",
})

const currency = useSelectedCurrency()
const isUniswapV2LpToken = token?.type === "evm-uniswapv2"

return (
<button
ref={refButton}
disabled={!allowUntransferable && !isTransferable}
title={
allowUntransferable || isTransferable
Expand All @@ -117,53 +177,49 @@ const TokenRow: FC<TokenRowProps> = ({
selected && "bg-grey-800 text-body-secondary",
)}
>
{intersection?.isIntersecting && (
<>
<div className="w-16 shrink-0">
<TokenLogo tokenId={token.id} className="!text-xl" />
<div className="w-16 shrink-0">
<TokenLogo tokenId={token.id} className="!text-xl" />
</div>
<div className="grow space-y-[5px]">
<div
className={classNames(
"flex w-full justify-between text-sm font-bold",
selected ? "text-body-secondary" : "text-body",
)}
>
<div className="flex items-center">
<span>{token.symbol}</span>
<TokenTypePill type={token.type} className="rounded-xs ml-3 px-1 py-0.5" />
{selected && <CheckCircleIcon className="ml-3 inline align-text-top" />}
</div>
<div className="grow space-y-[5px]">
<div
className={classNames(
"flex w-full justify-between text-sm font-bold",
selected ? "text-body-secondary" : "text-body",
)}
>
<div className="flex items-center">
<span>{token.symbol}</span>
<TokenTypePill type={token.type} className="rounded-xs ml-3 px-1 py-0.5" />
{selected && <CheckCircleIcon className="ml-3 inline align-text-top" />}
</div>
<div className={classNames(isLoading && "animate-pulse")}>
<Tokens
amount={tokensTotal}
decimals={token.decimals}
symbol={isUniswapV2LpToken ? "" : token.symbol}
isBalance
noCountUp
/>
</div>
</div>
<div className="text-body-secondary flex w-full items-center justify-between gap-2 text-right text-xs font-light">
<div className="flex flex-col justify-center">
<ChainLogoBase
logo={chainLogo}
name={chainName ?? ""}
className="inline-block text-sm"
/>
</div>
<div>{chainName}</div>
<div className={classNames("grow", isLoading && "animate-pulse")}>
{hasFiatRate ? (
<Fiat amount={balances.sum.fiat(currency).transferable} isBalance noCountUp />
) : (
"-"
)}
</div>
</div>
<div className={classNames(isLoading && "animate-pulse")}>
<Tokens
amount={tokensTotal}
decimals={token.decimals}
symbol={isUniswapV2LpToken ? "" : token.symbol}
isBalance
noCountUp
/>
</div>
</>
)}
</div>
<div className="text-body-secondary flex w-full items-center justify-between gap-2 text-right text-xs font-light">
<div className="flex flex-col justify-center">
<ChainLogoBase
logo={chainLogo}
name={chainName ?? ""}
className="inline-block text-sm"
/>
</div>
<div>{chainName}</div>
<div className={classNames("grow", isLoading && "animate-pulse")}>
{hasFiatRate ? (
<Fiat amount={balances.sum.fiat(currency).transferable} isBalance noCountUp />
) : (
"-"
)}
</div>
</div>
</div>
</button>
)
}
Expand Down Expand Up @@ -258,7 +314,7 @@ const TokensList: FC<TokensListProps> = ({
tokenRatesMap,
])

const tokensWithBalances = useMemo(() => {
const tokensWithBalances = useMemo<TokenData[]>(() => {
// wait until balances are loaded
if (!accountBalances.count) return []

Expand Down Expand Up @@ -336,9 +392,9 @@ const TokensList: FC<TokensListProps> = ({
})
}, [search, tokensWithBalances])

const handleAccountClick = useCallback(
(address: string) => () => {
onSelect?.(address)
const handleTokenClick = useCallback(
(tokenId: string) => {
onSelect?.(tokenId)
},
[onSelect],
)
Expand All @@ -347,19 +403,13 @@ const TokensList: FC<TokensListProps> = ({
<div className="min-h-full">
{accountBalances.count ? (
<>
{tokens?.map(({ token, balances, chainName, chainLogo, hasFiatRate }) => (
<TokenRow
key={token.id}
selected={token.id === selected}
token={token}
balances={balances}
chainName={chainName}
chainLogo={chainLogo}
hasFiatRate={hasFiatRate}
allowUntransferable={allowUntransferable}
onClick={handleAccountClick(token.id)}
/>
))}
<TokenRows
tokens={tokens}
selectedTokenId={selected}
onTokenClick={handleTokenClick}
allowUntransferable={allowUntransferable}
/>

{!tokens?.length && (
<div className="text-body-secondary flex h-[5.8rem] w-full items-center px-12 text-left">
{t("No token matches your search")}
Expand Down
Loading

0 comments on commit eb959d4

Please sign in to comment.