perf: 前端性能优化 - 构建分包、异步组件、渲染优化

This commit is contained in:
ViperEkura 2026-03-26 17:18:47 +08:00
parent 8c29f0684f
commit de79c227e2
7 changed files with 67 additions and 25 deletions

View File

@ -3,6 +3,9 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<title>Chat</title> <title>Chat</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head> </head>

View File

@ -57,22 +57,23 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, shallowRef, computed, onMounted, defineAsyncComponent, triggerRef } from 'vue'
import Sidebar from './components/Sidebar.vue' import Sidebar from './components/Sidebar.vue'
import ChatView from './components/ChatView.vue' import ChatView from './components/ChatView.vue'
import SettingsPanel from './components/SettingsPanel.vue'
import StatsPanel from './components/StatsPanel.vue' const SettingsPanel = defineAsyncComponent(() => import('./components/SettingsPanel.vue'))
const StatsPanel = defineAsyncComponent(() => import('./components/StatsPanel.vue'))
import { conversationApi, messageApi } from './api' import { conversationApi, messageApi } from './api'
// -- Conversations state -- // -- Conversations state --
const conversations = ref([]) const conversations = shallowRef([])
const currentConvId = ref(null) const currentConvId = ref(null)
const loadingConvs = ref(false) const loadingConvs = ref(false)
const hasMoreConvs = ref(false) const hasMoreConvs = ref(false)
const nextConvCursor = ref(null) const nextConvCursor = ref(null)
// -- Messages state -- // -- Messages state --
const messages = ref([]) const messages = shallowRef([])
const hasMoreMessages = ref(false) const hasMoreMessages = ref(false)
const loadingMessages = ref(false) const loadingMessages = ref(false)
const nextMsgCursor = ref(null) const nextMsgCursor = ref(null)
@ -81,8 +82,8 @@ const nextMsgCursor = ref(null)
const streaming = ref(false) const streaming = ref(false)
const streamContent = ref('') const streamContent = ref('')
const streamThinking = ref('') const streamThinking = ref('')
const streamToolCalls = ref([]) const streamToolCalls = shallowRef([])
const streamProcessSteps = ref([]) const streamProcessSteps = shallowRef([])
// //
const streamStates = new Map() const streamStates = new Map()
@ -151,7 +152,7 @@ async function loadConversations(reset = true) {
if (reset) { if (reset) {
conversations.value = res.data.items conversations.value = res.data.items
} else { } else {
conversations.value.push(...res.data.items) conversations.value = [...conversations.value, ...res.data.items]
} }
nextConvCursor.value = res.data.next_cursor nextConvCursor.value = res.data.next_cursor
hasMoreConvs.value = res.data.has_more hasMoreConvs.value = res.data.has_more
@ -173,7 +174,7 @@ async function createConversation() {
title: '新对话', title: '新对话',
project_id: currentProject.value?.id || null, project_id: currentProject.value?.id || null,
}) })
conversations.value.unshift(res.data) conversations.value = [res.data, ...conversations.value]
await selectConversation(res.data.id) await selectConversation(res.data.id)
} catch (e) { } catch (e) {
console.error('Failed to create conversation:', e) console.error('Failed to create conversation:', e)
@ -282,7 +283,7 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) {
if (currentConvId.value === convId) { if (currentConvId.value === convId) {
streaming.value = false streaming.value = false
messages.value.push({ messages.value = [...messages.value, {
id: data.message_id, id: data.message_id,
conversation_id: convId, conversation_id: convId,
role: 'assistant', role: 'assistant',
@ -292,16 +293,20 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) {
process_steps: streamProcessSteps.value.filter(Boolean), process_steps: streamProcessSteps.value.filter(Boolean),
token_count: data.token_count, token_count: data.token_count,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}) }]
resetStreamState() resetStreamState()
if (updateConvList) { if (updateConvList) {
const idx = conversations.value.findIndex(c => c.id === convId) const idx = conversations.value.findIndex(c => c.id === convId)
if (idx >= 0) { if (idx >= 0) {
const conv = idx > 0 ? conversations.value.splice(idx, 1)[0] : conversations.value[0] const conv = conversations.value[idx]
conv.message_count = (conv.message_count || 0) + 2 const updated = {
if (data.suggested_title) conv.title = data.suggested_title ...conv,
if (idx > 0) conversations.value.unshift(conv) message_count: (conv.message_count || 0) + 2,
...(data.suggested_title ? { title: data.suggested_title } : {}),
}
const newList = conversations.value.filter((_, i) => i !== idx)
conversations.value = [updated, ...newList]
} }
} }
} else { } else {
@ -309,11 +314,13 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) {
const res = await messageApi.list(convId, null, 50) const res = await messageApi.list(convId, null, 50)
const idx = conversations.value.findIndex(c => c.id === convId) const idx = conversations.value.findIndex(c => c.id === convId)
if (idx >= 0) { if (idx >= 0) {
conversations.value[idx].message_count = res.data.items.length const conv = conversations.value[idx]
const updates = { message_count: res.data.items.length }
if (res.data.items.length > 0) { if (res.data.items.length > 0) {
const convRes = await conversationApi.get(convId) const convRes = await conversationApi.get(convId)
if (convRes.data.title) conversations.value[idx].title = convRes.data.title if (convRes.data.title) updates.title = convRes.data.title
} }
conversations.value = conversations.value.map((c, i) => i === idx ? { ...c, ...updates } : c)
} }
} catch (_) {} } catch (_) {}
} }
@ -345,7 +352,7 @@ async function sendMessage(data) {
token_count: 0, token_count: 0,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
} }
messages.value.push(userMsg) messages.value = [...messages.value, userMsg]
initStreamState() initStreamState()
@ -410,7 +417,7 @@ async function saveSettings(data) {
const res = await conversationApi.update(currentConvId.value, data) const res = await conversationApi.update(currentConvId.value, data)
const idx = conversations.value.findIndex(c => c.id === currentConvId.value) const idx = conversations.value.findIndex(c => c.id === currentConvId.value)
if (idx !== -1) { if (idx !== -1) {
conversations.value[idx] = { ...conversations.value[idx], ...res.data } conversations.value = conversations.value.map((c, i) => i === idx ? { ...c, ...res.data } : c)
} }
} catch (e) { } catch (e) {
console.error('Failed to save settings:', e) console.error('Failed to save settings:', e)

View File

@ -42,6 +42,7 @@
v-for="msg in messages" v-for="msg in messages"
:key="msg.id" :key="msg.id"
:data-msg-id="msg.id" :data-msg-id="msg.id"
v-memo="[msg.text, msg.thinking, msg.tool_calls, msg.process_steps, msg.attachments]"
> >
<MessageBubble <MessageBubble
:role="msg.role" :role="msg.role"
@ -119,6 +120,7 @@ const inputRef = ref(null)
const modelNameMap = ref({}) const modelNameMap = ref({})
const activeMessageId = ref(null) const activeMessageId = ref(null)
let scrollObserver = null let scrollObserver = null
const observedElements = new WeakSet()
function formatModelName(modelId) { function formatModelName(modelId) {
return modelNameMap.value[modelId] || modelId return modelNameMap.value[modelId] || modelId
@ -158,7 +160,12 @@ watch(() => props.messages.length, () => {
nextTick(() => { nextTick(() => {
if (!scrollObserver || !scrollContainer.value) return if (!scrollObserver || !scrollContainer.value) return
const wrappers = scrollContainer.value.querySelectorAll('[data-msg-id]') const wrappers = scrollContainer.value.querySelectorAll('[data-msg-id]')
wrappers.forEach(el => scrollObserver.observe(el)) wrappers.forEach(el => {
if (!observedElements.has(el)) {
scrollObserver.observe(el)
observedElements.add(el)
}
})
}) })
}) })

View File

@ -211,8 +211,13 @@ const processItems = computed(() => {
return items return items
}) })
// processBlock processItems // processBlock
useCodeEnhancement(processRef, processItems, { deep: true }) const { enhance, debouncedEnhance } = useCodeEnhancement(processRef, processItems, { deep: true })
// 使 DOM
watch(() => props.streamingContent?.length, () => {
if (props.streaming) debouncedEnhance()
})
</script> </script>
<style scoped> <style scoped>

View File

@ -1,4 +1,4 @@
import { watch, onMounted, nextTick } from 'vue' import { watch, onMounted, nextTick, onUnmounted } from 'vue'
import { enhanceCodeBlocks } from '../utils/markdown' import { enhanceCodeBlocks } from '../utils/markdown'
/** /**
@ -10,15 +10,25 @@ import { enhanceCodeBlocks } from '../utils/markdown'
* @param {import('vue').WatchOptions} [watchOpts] - Optional watch options (e.g. { deep: true }) * @param {import('vue').WatchOptions} [watchOpts] - Optional watch options (e.g. { deep: true })
*/ */
export function useCodeEnhancement(templateRef, dep, watchOpts) { export function useCodeEnhancement(templateRef, dep, watchOpts) {
let debounceTimer = null
function enhance() { function enhance() {
enhanceCodeBlocks(templateRef.value) enhanceCodeBlocks(templateRef.value)
} }
function debouncedEnhance() {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => nextTick(enhance), 150)
}
onMounted(enhance) onMounted(enhance)
onUnmounted(() => {
if (debounceTimer) clearTimeout(debounceTimer)
})
if (dep) { if (dep) {
watch(dep, () => nextTick(enhance), watchOpts) watch(dep, () => nextTick(enhance), watchOpts)
} }
return { enhance } return { enhance, debouncedEnhance }
} }

View File

@ -1,5 +1,5 @@
/* highlight.js - Light theme for code blocks */ /* highlight.js - Light theme for code blocks */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap'); /* JetBrains Mono font loaded via <link> in index.html */
.hljs { .hljs {
color: #24292f; color: #24292f;

View File

@ -23,4 +23,14 @@ export default defineConfig({
}, },
}, },
}, },
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-markdown': ['marked', 'marked-highlight', 'highlight.js'],
'vendor-katex': ['katex'],
},
},
},
},
}) })