perf: Optimizing the image loading

Does no longer load all klipy images through image proxy from signal server. Improves loading performance.
This commit is contained in:
2026-03-30 00:10:40 +02:00
parent 11917f3412
commit eb23fd71ec
9 changed files with 65 additions and 19 deletions

View File

@@ -109,7 +109,7 @@ sequenceDiagram
## GIF integration ## GIF integration
`KlipyService` checks availability on the active server, then proxies search requests through the server API. Images are rendered via an image proxy endpoint to avoid mixed-content issues. `KlipyService` checks availability on the active server, then proxies search requests through the server API. Rendered remote images now attempt a direct load first and only fall back to the image proxy after the browser reports a load failure, which is the practical approximation of a CORS or mixed-content fallback path in the renderer.
```mermaid ```mermaid
graph LR graph LR

View File

@@ -135,6 +135,10 @@ export class KlipyService {
} }
buildRenderableImageUrl(url: string): string { buildRenderableImageUrl(url: string): string {
return this.normalizeMediaUrl(url);
}
buildImageProxyUrl(url: string): string {
const trimmed = this.normalizeMediaUrl(url); const trimmed = this.normalizeMediaUrl(url);
if (!trimmed) if (!trimmed)

View File

@@ -0,0 +1,50 @@
import {
Directive,
HostBinding,
HostListener,
effect,
inject,
input,
signal
} from '@angular/core';
import { KlipyService } from '../application/klipy.service';
@Directive({
selector: 'img[appChatImageProxyFallback]',
standalone: true
})
export class ChatImageProxyFallbackDirective {
readonly sourceUrl = input('', { alias: 'appChatImageProxyFallback' });
private readonly klipy = inject(KlipyService);
private readonly renderedSource = signal('');
private hasAppliedProxyFallback = false;
constructor() {
effect(() => {
this.hasAppliedProxyFallback = false;
this.renderedSource.set(this.klipy.buildRenderableImageUrl(this.sourceUrl()));
});
}
@HostBinding('src')
get src(): string {
return this.renderedSource();
}
@HostListener('error')
handleError(): void {
if (this.hasAppliedProxyFallback) {
return;
}
const proxyUrl = this.klipy.buildImageProxyUrl(this.sourceUrl());
if (!proxyUrl || proxyUrl === this.renderedSource()) {
return;
}
this.hasAppliedProxyFallback = true;
this.renderedSource.set(proxyUrl);
}
}

View File

@@ -206,7 +206,7 @@
<div class="group flex max-w-sm items-center gap-3 rounded-xl border border-border bg-secondary/60 px-2.5 py-2"> <div class="group flex max-w-sm items-center gap-3 rounded-xl border border-border bg-secondary/60 px-2.5 py-2">
<div class="relative h-12 w-12 overflow-hidden rounded-lg bg-secondary/80"> <div class="relative h-12 w-12 overflow-hidden rounded-lg bg-secondary/80">
<img <img
[src]="getPendingKlipyGifPreviewUrl()" [appChatImageProxyFallback]="pendingKlipyGif()!.previewUrl || pendingKlipyGif()!.url"
[alt]="pendingKlipyGif()!.title || 'KLIPY GIF'" [alt]="pendingKlipyGif()!.title || 'KLIPY GIF'"
class="h-full w-full object-cover" class="h-full w-full object-cover"
loading="lazy" loading="lazy"

View File

@@ -23,6 +23,7 @@ import type { ClipboardFilePayload } from '../../../../../../core/platform/elect
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service'; import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
import { KlipyGif, KlipyService } from '../../../../application/klipy.service'; import { KlipyGif, KlipyService } from '../../../../application/klipy.service';
import { Message } from '../../../../../../shared-kernel'; import { Message } from '../../../../../../shared-kernel';
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component'; import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component';
import { ChatMarkdownService } from '../../services/chat-markdown.service'; import { ChatMarkdownService } from '../../services/chat-markdown.service';
import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.models'; import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.models';
@@ -40,6 +41,7 @@ const DEFAULT_TEXTAREA_HEIGHT = 62;
CommonModule, CommonModule,
FormsModule, FormsModule,
NgIcon, NgIcon,
ChatImageProxyFallbackDirective,
TypingIndicatorComponent TypingIndicatorComponent
], ],
viewProviders: [ viewProviders: [
@@ -231,12 +233,6 @@ export class ChatMessageComposerComponent implements AfterViewInit, OnDestroy {
requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus()); requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus());
} }
getPendingKlipyGifPreviewUrl(): string {
const gif = this.pendingKlipyGif();
return gif ? this.klipy.buildRenderableImageUrl(gif.previewUrl || gif.url) : '';
}
formatBytes(bytes: number): string { formatBytes(bytes: number): string {
const units = [ const units = [
'B', 'B',

View File

@@ -98,7 +98,7 @@
> >
<div class="relative mt-2 inline-block overflow-hidden rounded-md border border-border/60 bg-secondary/20"> <div class="relative mt-2 inline-block overflow-hidden rounded-md border border-border/60 bg-secondary/20">
<img <img
[src]="getMarkdownImageSource(node.url)" [appChatImageProxyFallback]="node.url"
[alt]="node.alt || 'Shared image'" [alt]="node.alt || 'Shared image'"
class="block max-h-80 max-w-full w-auto" class="block max-h-80 max-w-full w-auto"
loading="lazy" loading="lazy"

View File

@@ -41,6 +41,7 @@ import {
ChatVideoPlayerComponent, ChatVideoPlayerComponent,
UserAvatarComponent UserAvatarComponent
} from '../../../../../../shared'; } from '../../../../../../shared';
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
import { import {
ChatMessageDeleteEvent, ChatMessageDeleteEvent,
ChatMessageEditEvent, ChatMessageEditEvent,
@@ -102,6 +103,7 @@ interface ChatMessageAttachmentViewModel extends Attachment {
ChatVideoPlayerComponent, ChatVideoPlayerComponent,
RemarkModule, RemarkModule,
MermaidComponent, MermaidComponent,
ChatImageProxyFallbackDirective,
UserAvatarComponent UserAvatarComponent
], ],
viewProviders: [ viewProviders: [
@@ -318,10 +320,6 @@ export class ChatMessageItemComponent {
); );
} }
getMarkdownImageSource(url?: string): string {
return url ? this.klipy.buildRenderableImageUrl(url) : '';
}
getMermaidCode(code?: string): string { getMermaidCode(code?: string): string {
return (code ?? '').replace(MERMAID_LINE_BREAK_PATTERN, '\n').trim(); return (code ?? '').replace(MERMAID_LINE_BREAK_PATTERN, '\n').trim();
} }

View File

@@ -93,7 +93,7 @@
[style.aspect-ratio]="gifAspectRatio(gif)" [style.aspect-ratio]="gifAspectRatio(gif)"
> >
<img <img
[src]="gifPreviewUrl(gif)" [appChatImageProxyFallback]="gif.previewUrl || gif.url"
[alt]="gif.title || 'KLIPY GIF'" [alt]="gif.title || 'KLIPY GIF'"
class="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.03]" class="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.03]"
loading="lazy" loading="lazy"

View File

@@ -21,6 +21,7 @@ import {
lucideX lucideX
} from '@ng-icons/lucide'; } from '@ng-icons/lucide';
import { KlipyGif, KlipyService } from '../../application/klipy.service'; import { KlipyGif, KlipyService } from '../../application/klipy.service';
import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive';
@Component({ @Component({
selector: 'app-klipy-gif-picker', selector: 'app-klipy-gif-picker',
@@ -28,7 +29,8 @@ import { KlipyGif, KlipyService } from '../../application/klipy.service';
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
NgIcon NgIcon,
ChatImageProxyFallbackDirective
], ],
viewProviders: [ viewProviders: [
provideIcons({ provideIcons({
@@ -112,10 +114,6 @@ export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy
return '1 / 1'; return '1 / 1';
} }
gifPreviewUrl(gif: KlipyGif): string {
return this.klipy.buildRenderableImageUrl(gif.previewUrl || gif.url);
}
private async loadResults(reset: boolean): Promise<void> { private async loadResults(reset: boolean): Promise<void> {
if (reset) { if (reset) {
this.currentPage = 1; this.currentPage = 1;