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>
<meta charset="UTF-8" />
<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>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>

View File

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

View File

@ -42,6 +42,7 @@
v-for="msg in messages"
:key="msg.id"
:data-msg-id="msg.id"
v-memo="[msg.text, msg.thinking, msg.tool_calls, msg.process_steps, msg.attachments]"
>
<MessageBubble
:role="msg.role"
@ -119,6 +120,7 @@ const inputRef = ref(null)
const modelNameMap = ref({})
const activeMessageId = ref(null)
let scrollObserver = null
const observedElements = new WeakSet()
function formatModelName(modelId) {
return modelNameMap.value[modelId] || modelId
@ -158,7 +160,12 @@ watch(() => props.messages.length, () => {
nextTick(() => {
if (!scrollObserver || !scrollContainer.value) return
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
})
// processBlock processItems
useCodeEnhancement(processRef, processItems, { deep: true })
// processBlock
const { enhance, debouncedEnhance } = useCodeEnhancement(processRef, processItems, { deep: true })
// 使 DOM
watch(() => props.streamingContent?.length, () => {
if (props.streaming) debouncedEnhance()
})
</script>
<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'
/**
@ -10,15 +10,25 @@ import { enhanceCodeBlocks } from '../utils/markdown'
* @param {import('vue').WatchOptions} [watchOpts] - Optional watch options (e.g. { deep: true })
*/
export function useCodeEnhancement(templateRef, dep, watchOpts) {
let debounceTimer = null
function enhance() {
enhanceCodeBlocks(templateRef.value)
}
function debouncedEnhance() {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => nextTick(enhance), 150)
}
onMounted(enhance)
onUnmounted(() => {
if (debounceTimer) clearTimeout(debounceTimer)
})
if (dep) {
watch(dep, () => nextTick(enhance), watchOpts)
}
return { enhance }
return { enhance, debouncedEnhance }
}

View File

@ -1,5 +1,5 @@
/* 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 {
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'],
},
},
},
},
})