mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
fix: 优化 Windows 启动性能与聊天图片缓存
- 增加聊天图片磁盘持久化缓存,重启后直接复用 - 将 DAT 图片解密移至 Worker,避免阻塞主进程 - 增加图片加载优先级和并发控制 - 优化会话目录与图片文件的异步查找 - 使用本地媒体协议加载缓存图片,减少 Base64 IPC 开销 - 优化启动缓存与数据库初始化流程,降低窗口未响应时间 (cherry picked from commit 82bcc8d32ca674de38c745cc9925ed2309887dc7)
This commit is contained in:
@@ -29,93 +29,92 @@ export function ImageBubble({
|
||||
const [usingFallback, setUsingFallback] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const mountedRef = useRef(true)
|
||||
const backgroundUpgradeRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true
|
||||
return () => {
|
||||
mountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const upgradeOriginalInBackground = useCallback(() => {
|
||||
if (!isThumbnail || backgroundUpgradeRef.current || (!imageMd5 && !imageDatName)) return
|
||||
backgroundUpgradeRef.current = true
|
||||
void requestImage(imageMd5, imageDatName, sessionId, { force: true }, 1)
|
||||
.then((original) => {
|
||||
if (!mountedRef.current) return
|
||||
setImageUrl(original.data)
|
||||
setIsThumbnail(false)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}, [imageDatName, imageMd5, isThumbnail, sessionId])
|
||||
|
||||
const loadImage = useCallback(async () => {
|
||||
if (imageUrl || loading) return
|
||||
if (!imageMd5 && !imageDatName) {
|
||||
if (fallbackUrl) {
|
||||
setImageUrl(fallbackUrl)
|
||||
setUsingFallback(true)
|
||||
setError(null)
|
||||
const loadImage = useCallback(
|
||||
async (priority = 0) => {
|
||||
if (imageUrl) return
|
||||
if (!imageMd5 && !imageDatName) {
|
||||
if (fallbackUrl) {
|
||||
setImageUrl(fallbackUrl)
|
||||
setUsingFallback(true)
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
setError('缺少图片标识')
|
||||
return
|
||||
}
|
||||
setError('缺少图片标识')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await requestImage(
|
||||
imageMd5,
|
||||
imageDatName,
|
||||
sessionId,
|
||||
{ preferThumbnail: true },
|
||||
0
|
||||
)
|
||||
setImageUrl(result.data)
|
||||
setUsingFallback(false)
|
||||
setIsThumbnail(result.isThumbnail)
|
||||
setError(null)
|
||||
if (result.isThumbnail) upgradeOriginalInBackground()
|
||||
} catch (error) {
|
||||
if (fallbackUrl) {
|
||||
setImageUrl(fallbackUrl)
|
||||
setUsingFallback(true)
|
||||
setError(null)
|
||||
} else {
|
||||
setError(error instanceof Error ? error.message : '加载图片失败')
|
||||
if (loading) {
|
||||
if (priority === 0) {
|
||||
void requestImage(
|
||||
imageMd5,
|
||||
imageDatName,
|
||||
sessionId,
|
||||
{ preferThumbnail: true },
|
||||
priority
|
||||
).catch(() => undefined)
|
||||
}
|
||||
return
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [
|
||||
fallbackUrl,
|
||||
imageDatName,
|
||||
imageMd5,
|
||||
imageUrl,
|
||||
loading,
|
||||
sessionId,
|
||||
upgradeOriginalInBackground
|
||||
])
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await requestImage(
|
||||
imageMd5,
|
||||
imageDatName,
|
||||
sessionId,
|
||||
{ preferThumbnail: true },
|
||||
priority
|
||||
)
|
||||
if (!mountedRef.current) return
|
||||
setImageUrl(result.data)
|
||||
setUsingFallback(false)
|
||||
setIsThumbnail(result.isThumbnail)
|
||||
setError(null)
|
||||
} catch (error) {
|
||||
if (!mountedRef.current) return
|
||||
if (fallbackUrl) {
|
||||
setImageUrl(fallbackUrl)
|
||||
setUsingFallback(true)
|
||||
setError(null)
|
||||
} else {
|
||||
setError(error instanceof Error ? error.message : '加载图片失败')
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false)
|
||||
}
|
||||
},
|
||||
[fallbackUrl, imageDatName, imageMd5, imageUrl, loading, sessionId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (initialCachedImage?.isThumbnail) upgradeOriginalInBackground()
|
||||
}, [initialCachedImage?.isThumbnail, upgradeOriginalInBackground])
|
||||
|
||||
useEffect(() => {
|
||||
if (imageUrl || loading || error) return
|
||||
if (imageUrl || error) return
|
||||
const element = containerRef.current
|
||||
if (!element || typeof IntersectionObserver === 'undefined') {
|
||||
const timer = window.setTimeout(() => void loadImage(), 0)
|
||||
const timer = window.setTimeout(() => void loadImage(0), 0)
|
||||
return () => window.clearTimeout(timer)
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (!entries.some((entry) => entry.isIntersecting)) return
|
||||
const entry = entries.find((candidate) => candidate.isIntersecting)
|
||||
if (!entry) return
|
||||
const rect = entry.boundingClientRect
|
||||
const isInViewport =
|
||||
rect.bottom >= 0 &&
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.right >= 0 &&
|
||||
rect.left <= window.innerWidth
|
||||
observer.disconnect()
|
||||
void loadImage()
|
||||
void loadImage(isInViewport ? 0 : 1)
|
||||
},
|
||||
{ rootMargin: '400px 0px' }
|
||||
{ rootMargin: loading ? '0px' : '400px 0px' }
|
||||
)
|
||||
observer.observe(element)
|
||||
return () => observer.disconnect()
|
||||
@@ -143,7 +142,7 @@ export function ImageBubble({
|
||||
setUpgrading(true)
|
||||
try {
|
||||
const result = await requestImage(imageMd5, imageDatName, sessionId, { force: true }, 0)
|
||||
if (result.data.startsWith('data:image/')) {
|
||||
if (result.data.startsWith('data:image/') || result.data.startsWith('wxe-media://')) {
|
||||
setImageUrl(result.data)
|
||||
setUsingFallback(false)
|
||||
setIsThumbnail(result.isThumbnail)
|
||||
@@ -162,7 +161,7 @@ export function ImageBubble({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="image-bubble image-loading" onClick={handleClick}>
|
||||
<div ref={containerRef} className="image-bubble image-loading" onClick={handleClick}>
|
||||
<div className="image-loading-skeleton" aria-hidden />
|
||||
<div className="image-quality-badge">加载中</div>
|
||||
</div>
|
||||
@@ -171,7 +170,7 @@ export function ImageBubble({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="image-bubble image-error" onClick={loadImage}>
|
||||
<div className="image-bubble image-error" onClick={() => void loadImage(0)}>
|
||||
<div className="image-error-text">{error || '图片未缓存'}</div>
|
||||
<div className="image-quality-badge">加载失败</div>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,17 @@ export type ImageLoadOptions = {
|
||||
}
|
||||
|
||||
type QueueItem = {
|
||||
requestKey: string
|
||||
priority: number
|
||||
run: () => Promise<LoadedImage>
|
||||
resolve: (value: LoadedImage) => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_IMAGE_LOADS = 3
|
||||
// Cache probes are cheap asynchronous IPC calls. Cold decrypts are serialized
|
||||
// in the main process, so renderer concurrency only controls how quickly disk
|
||||
// cache hits can become visible after a restart.
|
||||
const MAX_CONCURRENT_IMAGE_LOADS = 32
|
||||
const MAX_IMAGE_CACHE_BYTES = 48 * 1024 * 1024
|
||||
const imageCache = new Map<string, LoadedImage>()
|
||||
const imageCacheSizes = new Map<string, number>()
|
||||
@@ -99,6 +103,8 @@ function cacheImage(
|
||||
}
|
||||
|
||||
function pumpImageQueue(): void {
|
||||
if (activeImageLoads >= MAX_CONCURRENT_IMAGE_LOADS || imageQueue.length === 0) return
|
||||
|
||||
while (activeImageLoads < MAX_CONCURRENT_IMAGE_LOADS && imageQueue.length > 0) {
|
||||
imageQueue.sort((left, right) => left.priority - right.priority)
|
||||
const item = imageQueue.shift()
|
||||
@@ -114,6 +120,10 @@ function pumpImageQueue(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function isSupportedImageUrl(value: string | undefined): value is string {
|
||||
return Boolean(value?.startsWith('data:image/') || value?.startsWith('wxe-media://'))
|
||||
}
|
||||
|
||||
export function getCachedLoadedImage(
|
||||
imageMd5?: string,
|
||||
imageDatName?: string,
|
||||
@@ -136,16 +146,24 @@ export function requestImage(
|
||||
if (!identity) return Promise.reject(new Error('缺少图片标识'))
|
||||
const requestKey = `${identity}:${cacheMode(options)}`
|
||||
const existingRequest = imageRequests.get(requestKey)
|
||||
if (existingRequest) return existingRequest
|
||||
if (existingRequest) {
|
||||
const queuedItem = imageQueue.find((item) => item.requestKey === requestKey)
|
||||
if (queuedItem && priority < queuedItem.priority) queuedItem.priority = priority
|
||||
return existingRequest
|
||||
}
|
||||
|
||||
const request = new Promise<LoadedImage>((resolve, reject) => {
|
||||
imageQueue.push({
|
||||
requestKey,
|
||||
priority,
|
||||
resolve,
|
||||
reject,
|
||||
run: async () => {
|
||||
const result = await window.api.getImage(imageMd5, imageDatName, sessionId, options)
|
||||
if (!result.success || !result.data?.startsWith('data:image/')) {
|
||||
const result = await window.api.getImage(imageMd5, imageDatName, sessionId, {
|
||||
...options,
|
||||
priority
|
||||
})
|
||||
if (!result.success || !isSupportedImageUrl(result.data)) {
|
||||
throw new Error(result.error || '加载图片失败')
|
||||
}
|
||||
const loadedImage = {
|
||||
|
||||
Reference in New Issue
Block a user