Skip to content

Commit 82c5c94

Browse files
committed
perf: Improve message rendering
1 parent 9f5d5f2 commit 82c5c94

4 files changed

Lines changed: 94 additions & 83 deletions

File tree

Binary file not shown.

Sidekick/Views/Chat/Conversation/Messages/Message/MessageContentView.swift

Lines changed: 86 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -9,89 +9,93 @@ import LaTeXSwiftUI
99
import MarkdownUI
1010
import Splash
1111
import SwiftUI
12-
import Shimmer
1312

1413
struct MessageContentView: View {
15-
16-
@Environment(\.colorScheme) private var colorScheme
1714

18-
@State private var cachedMarkdown: AnyView? // Cache rendered markdown view
19-
20-
var text: String
21-
22-
var textLatexProcessed: String {
23-
return self.text.convertLaTeX()
24-
}
25-
26-
let imageScaleFactor: CGFloat = 1.0
27-
28-
private var theme: Splash.Theme {
29-
switch self.colorScheme {
30-
case .dark: return .wwdc17(withFont: .init(size: 16))
31-
default: return .sunset(withFont: .init(size: 16))
32-
}
33-
}
34-
35-
var body: some View {
36-
VStack(alignment: .leading) {
37-
self.content
38-
}
39-
.onChange(of: self.textLatexProcessed) { _, newText in
40-
self.updateCachedMarkdown(newText) // Only redraw when text changes
41-
}
42-
.onAppear {
43-
if self.cachedMarkdown == nil {
44-
self.updateCachedMarkdown(
45-
self.textLatexProcessed
46-
) // Cache on appear
47-
}
48-
}
49-
}
50-
51-
@ViewBuilder
52-
var content: some View {
53-
if let cached = cachedMarkdown {
54-
cached // Used cached view if available
55-
} else {
56-
self.markdownView(
57-
self.textLatexProcessed
58-
)
59-
}
60-
}
61-
62-
@ViewBuilder
63-
private func markdownView(_ text: String) -> some View {
64-
Markdown(MarkdownContent(text))
65-
.markdownTheme(.gitHub)
66-
.markdownCodeSyntaxHighlighter(.splash(theme: self.theme))
67-
.markdownImageProvider(
68-
MarkdownImageProvider(scaleFactor: imageScaleFactor)
69-
)
70-
.markdownInlineImageProvider(
71-
MarkdownInlineImageProvider(
72-
scaleFactor: imageScaleFactor
73-
)
74-
)
75-
.textSelection(.enabled)
76-
}
77-
78-
private func updateCachedMarkdown(_ newText: String) {
79-
self.cachedMarkdown = AnyView(
80-
Markdown(MarkdownContent(newText))
81-
.markdownTheme(.gitHub)
82-
.markdownCodeSyntaxHighlighter(.splash(theme: self.theme))
83-
.markdownImageProvider(
84-
MarkdownImageProvider(
85-
scaleFactor: imageScaleFactor
86-
)
87-
)
88-
.markdownInlineImageProvider(
89-
MarkdownInlineImageProvider(
90-
scaleFactor: imageScaleFactor
91-
)
92-
)
93-
.textSelection(.enabled)
94-
)
95-
}
96-
15+
@Environment(\.colorScheme) private var colorScheme
16+
17+
// Asynchronously rendered markdown cached view.
18+
@State private var cachedMarkdown: AnyView? = nil
19+
// Stores the processed text after LaTeX conversion.
20+
@State private var processedText: String = ""
21+
// Used to debounce rapid changes.
22+
@State private var debounceWorkItem: DispatchWorkItem?
23+
24+
var text: String
25+
private let imageScaleFactor: CGFloat = 1.0
26+
27+
// Computes the code highlighting theme from the color scheme.
28+
private var theme: Splash.Theme {
29+
switch colorScheme {
30+
case .dark:
31+
return .wwdc17(withFont: .init(size: 16))
32+
default:
33+
return .sunset(withFont: .init(size: 16))
34+
}
35+
}
36+
37+
var body: some View {
38+
VStack(alignment: .leading) {
39+
// Always render the inline markdown immediately using processedText
40+
// This provides quick visual feedback while the cached version is updated
41+
if let cached = cachedMarkdown {
42+
cached
43+
} else {
44+
markdownView(processedText.isEmpty ? text.convertLaTeX() : processedText)
45+
}
46+
}
47+
.onAppear {
48+
// Immediately update processed text and asynchronously update the cached markdown
49+
updateProcessedTextAndMarkdown(text)
50+
}
51+
.onChange(of: self.text) { _, newText in
52+
updateProcessedTextAndMarkdown(newText)
53+
}
54+
}
55+
56+
/// Function to render a markdown view for the provided text
57+
@ViewBuilder
58+
private func markdownView(_ text: String) -> some View {
59+
Markdown(MarkdownContent(text))
60+
.markdownTheme(.gitHub)
61+
.markdownCodeSyntaxHighlighter(.splash(theme: theme))
62+
.markdownImageProvider(MarkdownImageProvider(scaleFactor: imageScaleFactor))
63+
.markdownInlineImageProvider(MarkdownInlineImageProvider(scaleFactor: imageScaleFactor))
64+
.textSelection(.enabled)
65+
}
66+
67+
/// Function to update the processed text and debounce the cached markdown update
68+
private func updateProcessedTextAndMarkdown(_ newText: String) {
69+
let converted = newText.convertLaTeX()
70+
// Update inline view immediately.
71+
processedText = converted
72+
73+
// Cancel any pending update.
74+
debounceWorkItem?.cancel()
75+
76+
// Shorten debounce time to reduce perceived delay (e.g., 0.1 second).
77+
let workItem = DispatchWorkItem { [converted] in
78+
updateCachedMarkdown(with: converted)
79+
}
80+
debounceWorkItem = workItem
81+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
82+
}
83+
84+
/// Function to asynchronously render and update the cached markdown view.
85+
private func updateCachedMarkdown(with text: String) {
86+
DispatchQueue.global(qos: .userInitiated).async {
87+
let rendered = AnyView(
88+
Markdown(MarkdownContent(text))
89+
.markdownTheme(.gitHub)
90+
.markdownCodeSyntaxHighlighter(.splash(theme: self.theme))
91+
.markdownImageProvider(MarkdownImageProvider(scaleFactor: self.imageScaleFactor))
92+
.markdownInlineImageProvider(MarkdownInlineImageProvider(scaleFactor: self.imageScaleFactor))
93+
.textSelection(.enabled)
94+
)
95+
DispatchQueue.main.async {
96+
self.cachedMarkdown = rendered
97+
}
98+
}
99+
}
100+
97101
}

Sidekick/Views/Chat/Conversation/Messages/Message/MessageView.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ import MarkdownUI
1010
import Splash
1111
import SwiftUI
1212

13-
struct MessageView: View {
13+
struct MessageView: View, Equatable {
1414

15+
static func == (lhs: MessageView, rhs: MessageView) -> Bool {
16+
lhs.message.id == rhs.message.id &&
17+
lhs.message.text == rhs.message.text &&
18+
lhs.message.functionCallRecords == rhs.message.functionCallRecords
19+
}
20+
1521
init(
1622
message: Message,
1723
canEdit: Bool = true,

Sidekick/Views/Chat/Conversation/Messages/MessagesView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ struct MessagesView: View {
5858
self.messages
5959
) { message in
6060
MessageView(message: message)
61+
.equatable()
6162
.id(message.id)
6263
}
6364
}

0 commit comments

Comments
 (0)