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
`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
graph LR

View File

@@ -135,6 +135,10 @@ export class KlipyService {
}
buildRenderableImageUrl(url: string): string {
return this.normalizeMediaUrl(url);
}
buildImageProxyUrl(url: string): string {
const trimmed = this.normalizeMediaUrl(url);
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="relative h-12 w-12 overflow-hidden rounded-lg bg-secondary/80">
<img
[src]="getPendingKlipyGifPreviewUrl()"
[appChatImageProxyFallback]="pendingKlipyGif()!.previewUrl || pendingKlipyGif()!.url"
[alt]="pendingKlipyGif()!.title || 'KLIPY GIF'"
class="h-full w-full object-cover"
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 { KlipyGif, KlipyService } from '../../../../application/klipy.service';
import { Message } from '../../../../../../shared-kernel';
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component';
import { ChatMarkdownService } from '../../services/chat-markdown.service';
import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.models';
@@ -40,6 +41,7 @@ const DEFAULT_TEXTAREA_HEIGHT = 62;
CommonModule,
FormsModule,
NgIcon,
ChatImageProxyFallbackDirective,
TypingIndicatorComponent
],
viewProviders: [
@@ -231,12 +233,6 @@ export class ChatMessageComposerComponent implements AfterViewInit, OnDestroy {
requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus());
}
getPendingKlipyGifPreviewUrl(): string {
const gif = this.pendingKlipyGif();
return gif ? this.klipy.buildRenderableImageUrl(gif.previewUrl || gif.url) : '';
}
formatBytes(bytes: number): string {
const units = [
'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">
<img
[src]="getMarkdownImageSource(node.url)"
[appChatImageProxyFallback]="node.url"
[alt]="node.alt || 'Shared image'"
class="block max-h-80 max-w-full w-auto"
loading="lazy"

View File

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

View File

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

View File

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