perf: eliminate loading flash with delayed loading state
- Implement timeout-based loading state (100ms delay) - Only show loading spinner for genuinely slow operations - Cancel timeout if generation completes quickly - Prevent flash for cached/fast renders while preserving UX for slow ones 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
||||
import { generateSorobanSVG, getWasmStatus, triggerWasmPreload, type SorobanConfig } from '@/lib/typst-soroban'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
|
||||
@@ -33,11 +33,60 @@ export function TypstSoroban({
|
||||
const globalConfig = useAbacusConfig()
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
const currentConfigRef = useRef<any>(null)
|
||||
|
||||
// Memoize the config to prevent unnecessary re-renders
|
||||
const stableConfig = useMemo(() => ({
|
||||
beadShape: globalConfig.beadShape,
|
||||
colorScheme: globalConfig.colorScheme,
|
||||
hideInactiveBeads: globalConfig.hideInactiveBeads,
|
||||
coloredNumerals: globalConfig.coloredNumerals,
|
||||
scaleFactor: globalConfig.scaleFactor
|
||||
}), [
|
||||
globalConfig.beadShape,
|
||||
globalConfig.colorScheme,
|
||||
globalConfig.hideInactiveBeads,
|
||||
globalConfig.coloredNumerals,
|
||||
globalConfig.scaleFactor
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldLoad) return
|
||||
|
||||
console.log(`🔄 TypstSoroban useEffect triggered for number ${number}, hasExistingSVG: ${!!svg}`)
|
||||
|
||||
async function generateSVG() {
|
||||
// Create current config signature
|
||||
const currentConfig = {
|
||||
number,
|
||||
width,
|
||||
height,
|
||||
beadShape: stableConfig.beadShape,
|
||||
colorScheme: stableConfig.colorScheme,
|
||||
hideInactiveBeads: stableConfig.hideInactiveBeads,
|
||||
coloredNumerals: stableConfig.coloredNumerals,
|
||||
scaleFactor: stableConfig.scaleFactor,
|
||||
enableServerFallback
|
||||
}
|
||||
|
||||
// Check if config changed since last render
|
||||
const configChanged = JSON.stringify(currentConfig) !== JSON.stringify(currentConfigRef.current)
|
||||
|
||||
// Don't regenerate if we already have an SVG for this exact config
|
||||
if (svg && !error && !configChanged) {
|
||||
console.log(`✅ Skipping regeneration for ${number} - already have SVG with same config`)
|
||||
return
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
console.log(`🔄 Config changed for ${number}, regenerating SVG`)
|
||||
// Clear existing SVG to show fresh loading state for config changes
|
||||
setSvg(null)
|
||||
}
|
||||
|
||||
// Update config ref
|
||||
currentConfigRef.current = currentConfig
|
||||
|
||||
// Cancel any previous generation
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort()
|
||||
@@ -46,34 +95,51 @@ export function TypstSoroban({
|
||||
abortControllerRef.current = new AbortController()
|
||||
const signal = abortControllerRef.current.signal
|
||||
|
||||
setIsLoading(true)
|
||||
// Check cache quickly before showing loading state
|
||||
const cacheKey = JSON.stringify({
|
||||
number,
|
||||
width,
|
||||
height,
|
||||
beadShape: stableConfig.beadShape,
|
||||
colorScheme: stableConfig.colorScheme,
|
||||
hideInactiveBeads: stableConfig.hideInactiveBeads,
|
||||
coloredNumerals: stableConfig.coloredNumerals,
|
||||
scaleFactor: stableConfig.scaleFactor,
|
||||
enableServerFallback
|
||||
})
|
||||
|
||||
setError(null)
|
||||
|
||||
// Longer delay to prevent flashing for fast renders
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
try {
|
||||
// Try generation immediately to see if it's cached
|
||||
const config: SorobanConfig = {
|
||||
number,
|
||||
width,
|
||||
height,
|
||||
beadShape: globalConfig.beadShape,
|
||||
colorScheme: globalConfig.colorScheme,
|
||||
hideInactiveBeads: globalConfig.hideInactiveBeads,
|
||||
coloredNumerals: globalConfig.coloredNumerals,
|
||||
scaleFactor: globalConfig.scaleFactor,
|
||||
beadShape: stableConfig.beadShape,
|
||||
colorScheme: stableConfig.colorScheme,
|
||||
hideInactiveBeads: stableConfig.hideInactiveBeads,
|
||||
coloredNumerals: stableConfig.coloredNumerals,
|
||||
scaleFactor: stableConfig.scaleFactor,
|
||||
transparent: false,
|
||||
enableServerFallback
|
||||
}
|
||||
|
||||
// Set loading only after a delay if generation is slow
|
||||
const loadingTimeout = setTimeout(() => {
|
||||
if (!signal.aborted) {
|
||||
setIsLoading(true)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const generatedSvg = await generateSorobanSVG(config)
|
||||
|
||||
// Clear timeout since we got result quickly
|
||||
clearTimeout(loadingTimeout)
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
setSvg(generatedSvg)
|
||||
// Call success callback after state is set
|
||||
setTimeout(() => onSuccess?.(), 0)
|
||||
} catch (err) {
|
||||
if (signal.aborted) return
|
||||
@@ -81,7 +147,6 @@ export function TypstSoroban({
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(errorMessage)
|
||||
console.error('TypstSoroban generation error:', err)
|
||||
// Call error callback after state is set
|
||||
setTimeout(() => onError?.(errorMessage), 0)
|
||||
} finally {
|
||||
if (!signal.aborted) {
|
||||
@@ -97,7 +162,7 @@ export function TypstSoroban({
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
}
|
||||
}, [shouldLoad, number, width, height, globalConfig.beadShape, globalConfig.colorScheme, globalConfig.hideInactiveBeads, globalConfig.coloredNumerals, globalConfig.scaleFactor, enableServerFallback])
|
||||
}, [shouldLoad, number, width, height, stableConfig, enableServerFallback])
|
||||
|
||||
// Handler to trigger loading on user interaction
|
||||
const handleLoadTrigger = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user