"Soft preload" icons (#3507)

This PR includes a "soft preload" feature for icons, where icons will be
loaded when the canvas first mounts. The component will not wait for
icons to finish loading before showing the editor, but this should help
with "pop in" on menu icons.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features

### Test Plan

1. Load the component
2. After load, open a menu for the first time
3. The icons should immediately be visible

### Release Notes

- Improve icon preloading
This commit is contained in:
Steve Ruiz 2024-04-17 11:57:08 +01:00 committed by GitHub
parent a253af95d9
commit 1450454873
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 19 additions and 43 deletions

View file

@ -19,7 +19,7 @@ body:
id: reproduction id: reproduction
attributes: attributes:
label: How can we reproduce the bug? label: How can we reproduce the bug?
description: If you can make the bug happen again, please share the steps involved. description: If you can make the bug happen again, please share the steps involved. You can [fork this CodeSandbox](https://codesandbox.io/p/sandbox/tldraw-example-n539u) to make a reproduction.
validations: validations:
required: false required: false
- type: dropdown - type: dropdown

View file

@ -11,6 +11,10 @@ import { TextDecoder, TextEncoder } from 'util'
global.TextEncoder = TextEncoder global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder global.TextDecoder = TextDecoder
Image.prototype.decode = async function () {
return true
}
function convertNumbersInObject(obj: any, roundToNearest: number) { function convertNumbersInObject(obj: any, roundToNearest: number) {
if (!obj) return obj if (!obj) return obj
if (Array.isArray(obj)) { if (Array.isArray(obj)) {

View file

@ -109,13 +109,10 @@ export function Tldraw(props: TldrawProps) {
) )
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls) const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets) const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
if (preloadingError) { if (preloadingError) {
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen> return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
} }
if (!preloadingComplete) { if (!preloadingComplete) {
return <LoadingScreen>Loading assets...</LoadingScreen> return <LoadingScreen>Loading assets...</LoadingScreen>
} }

View file

@ -1,4 +1,4 @@
import { createContext, useContext } from 'react' import { createContext, useContext, useEffect } from 'react'
import { TLUiAssetUrls } from '../assetUrls' import { TLUiAssetUrls } from '../assetUrls'
/** @internal */ /** @internal */
@ -14,6 +14,19 @@ export function AssetUrlsProvider({
assetUrls: TLUiAssetUrls assetUrls: TLUiAssetUrls
children: React.ReactNode children: React.ReactNode
}) { }) {
useEffect(() => {
for (const src of Object.values(assetUrls.icons)) {
const image = new Image()
image.src = src
image.decode()
}
for (const src of Object.values(assetUrls.embedIcons)) {
const image = new Image()
image.src = src
image.decode()
}
}, [assetUrls])
return <AssetUrlsContext.Provider value={assetUrls}>{children}</AssetUrlsContext.Provider> return <AssetUrlsContext.Provider value={assetUrls}>{children}</AssetUrlsContext.Provider>
} }

View file

@ -1,38 +0,0 @@
import { useEffect, useState } from 'react'
import { useAssetUrls } from '../context/asset-urls'
import { iconTypes } from '../icon-types'
/** @internal */
export function usePreloadIcons(): boolean {
const [isLoaded, setIsLoaded] = useState<boolean>(false)
const assetUrls = useAssetUrls()
useEffect(() => {
let cancelled = false
async function loadImages() {
// Run through all of the icons and load them. It doesn't matter
// if any of the images don't load; though we expect that they would.
// Instead, we just want to make sure that the browser has cached
// all of the icons it can so that they're available when we need them.
await Promise.allSettled(
iconTypes.map((icon) => {
const image = new Image()
image.src = assetUrls.icons[icon]
return image.decode()
})
)
if (cancelled) return
setIsLoaded(true)
}
loadImages()
return () => {
cancelled = true
}
}, [isLoaded, assetUrls])
return isLoaded
}