Skip to content

Commit fcef15c

Browse files
author
Mateus Medeiros
committed
Performance improvements
1 parent 905eb8d commit fcef15c

9 files changed

Lines changed: 154 additions & 68 deletions

File tree

src/webapp/src/app/app.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { RouterOutlet } from '@angular/router';
55
import { CommonModule } from '@angular/common';
66
import { UnsupportedScreenComponent } from './shared/unsupported-screen/unsupported-screen.component';
77
import { BrowserService } from './core/services/browser.service';
8+
import { MatIconRegistry } from '@angular/material/icon';
9+
import { Insights } from '../telemetry/insights.service';
810

911
@Component({
1012
selector: 'app-root',
@@ -28,8 +30,13 @@ export class AppComponent {
2830

2931
private platformId = inject(PLATFORM_ID);
3032
private browserService = inject(BrowserService);
33+
private matIconRegistry = inject(MatIconRegistry);
34+
private insights = inject(Insights);
3135

3236
constructor() {
37+
this.matIconRegistry.setDefaultFontSetClass('material-symbols-outlined');
38+
void this.insights;
39+
3340
// Check initial screen size only in browser
3441
if (isPlatformBrowser(this.platformId)) {
3542
this.checkScreenSize();

src/webapp/src/app/app.config.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
22
import { provideRouter } from '@angular/router';
33
import { provideHttpClient, withFetch, withInterceptorsFromDi } from '@angular/common/http';
4-
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
4+
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
55
import { environment } from '../enviroments/enviroment';
6-
import { InsightsModule } from '../telemetry/insights.module';
76
import { appRoutes } from './app.routes';
87
import { provideApi } from 'src/api/listen-and-write/provide-api';
98

@@ -13,9 +12,6 @@ export const appConfig: ApplicationConfig = {
1312
provideHttpClient(withInterceptorsFromDi(), withFetch()),
1413
provideApi(environment.apiUrl),
1514
provideZoneChangeDetection(),
16-
...(environment.production && typeof window !== 'undefined'
17-
? [importProvidersFrom(InsightsModule)]
18-
: []),
1915
provideClientHydration(withEventReplay()),
2016
],
21-
}
17+
}

src/webapp/src/app/home/home.component.html

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,27 @@ <h1 class="hero-headline text-display">
7272
</div>
7373
</div>
7474
<div class="mockup-content">
75+
<img
76+
class="mockup-poster"
77+
src="assets/hero-poster.webp"
78+
width="1280"
79+
height="720"
80+
alt="WriteFluency exercise demo showing listen and write interface"
81+
fetchpriority="high"
82+
loading="eager"
83+
decoding="async" />
7584
<video
7685
class="mockup-gif"
86+
[class.is-ready]="isHeroVideoReady"
7787
width="1280"
7888
height="720"
7989
autoplay
8090
muted
8191
loop
8292
playsinline
83-
preload="metadata"
84-
poster="assets/hero-poster.webp"
85-
aria-label="WriteFluency exercise demo showing listen and write interface">
93+
preload="none"
94+
aria-hidden="true"
95+
(loadeddata)="onHeroVideoLoaded()">
8696
<source src="assets/hero.mp4" type="video/mp4" />
8797
</video>
8898
</div>

src/webapp/src/app/home/home.component.scss

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,34 @@
212212
display: flex;
213213
justify-content: center;
214214
align-items: center;
215+
position: relative;
216+
overflow: hidden;
217+
}
218+
219+
.mockup-poster {
220+
width: 100%;
221+
height: auto;
222+
border-bottom-left-radius: 8px;
223+
border-bottom-right-radius: 8px;
224+
display: block;
225+
object-fit: contain;
215226
}
216227

217228
.mockup-gif {
218-
width: 100%;
219-
height: auto;
220-
border-bottom-left-radius: 8px;
221-
border-bottom-right-radius: 8px;
222-
display: block;
223-
object-fit: contain;
229+
position: absolute;
230+
inset: 0;
231+
width: 100%;
232+
height: auto;
233+
border-bottom-left-radius: 8px;
234+
border-bottom-right-radius: 8px;
235+
display: block;
236+
object-fit: contain;
237+
opacity: 0;
238+
transition: opacity 200ms ease;
239+
240+
&.is-ready {
241+
opacity: 1;
242+
}
224243
}
225244

226245
// ========================

src/webapp/src/app/home/home.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { SeoService } from '../core/services/seo.service';
2727
})
2828
export class HomeComponent implements OnInit {
2929
private seoService = inject(SeoService);
30+
isHeroVideoReady = false;
3031

3132
constructor(private browserService: BrowserService) {}
3233

@@ -54,4 +55,8 @@ export class HomeComponent implements OnInit {
5455
scrollToTop(): void {
5556
this.browserService.scrollToTop();
5657
}
58+
59+
onHeroVideoLoaded(): void {
60+
this.isHeroVideoReady = true;
61+
}
5762
}

src/webapp/src/index.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@
1414
<!-- Favicon -->
1515
<link rel="icon" type="image/x-icon" href="assets/app-icon.svg">
1616
<link rel="apple-touch-icon" href="assets/app-icon.svg">
17+
18+
<!-- LCP poster preload -->
19+
<link rel="preload" as="image" href="assets/hero-poster.webp" type="image/webp" fetchpriority="high">
1720

1821
<!-- Preconnect to external domains for performance -->
1922
<link rel="preconnect" href="https://fonts.googleapis.com">
2023
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2124

2225
<!-- Fonts -->
23-
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;500;600;700&display=swap" rel="stylesheet">
24-
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
25-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
26+
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700&display=swap" rel="stylesheet">
27+
<link
28+
rel="stylesheet"
29+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=account_balance,article,bolt,business_center,check_circle,circle,clear_all,computer,edit,error_outline,fast_forward,fast_rewind,flash_on,flight,grid_view,headphones,health_and_safety,home,info,library_books,looks_3,looks_one,looks_two,mail,newspaper,person,play_arrow,play_circle_outline,public,restaurant,science,search_off,sports_soccer,theater_comedy,timer,tips_and_updates,today,track_changes,trending_up,work&display=swap" />
2630

2731
</head>
2832
<body>

src/webapp/src/main.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { bootstrapApplication } from '@angular/platform-browser';
22
import { AppComponent } from './app/app.component';
33
import { appConfig } from './app/app.config';
4+
import { environment } from './enviroments/enviroment';
45

5-
import './telemetry/instrument';
6+
if (!environment.production) {
7+
void import('./telemetry/instrument');
8+
}
69

7-
bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));
10+
bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));

src/webapp/src/styles.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ body {
1717
font-family: 'Source Sans 3', sans-serif !important;
1818
}
1919

20+
.mat-icon.material-symbols-outlined {
21+
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
22+
}
23+
2024
:root {
2125
@include mat.toolbar-overrides((
2226
standard-height: vars.$nav-bar-height,
@@ -155,4 +159,4 @@ p, h1, h2, h3, h4, h5, h6, span, label {
155159
/* Overlay */
156160
.shepherd-modal-overlay-container.shepherd-modal-is-visible {
157161
background: rgba(0, 0, 0, 0.074);
158-
}
162+
}
Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,116 @@
11
import { ErrorHandler, Injectable } from '@angular/core';
22
import { Router } from '@angular/router';
3-
import { AngularPlugin } from '@microsoft/applicationinsights-angularplugin-js';
4-
import { ApplicationInsights, DistributedTracingModes } from '@microsoft/applicationinsights-web';
53
import { environment } from '../enviroments/enviroment';
6-
7-
@Injectable()
4+
5+
type MinimalAppInsights = {
6+
addTelemetryInitializer: (cb: (envelope: { tags?: Record<string, string> }) => void) => void;
7+
loadAppInsights: () => void;
8+
trackEvent: (event: { name: string }) => void;
9+
trackTrace: (trace: { message: string }) => void;
10+
};
11+
12+
@Injectable({ providedIn: 'root' })
813
export class Insights {
9-
private angularPlugin = new AngularPlugin();
1014
private initialized = false;
11-
private appInsights = new ApplicationInsights({
12-
config: {
13-
instrumentationKey: environment.instrumentationKey,
14-
distributedTracingMode: DistributedTracingModes.AI_AND_W3C,
15-
enableAutoRouteTracking: true,
16-
enableCorsCorrelation: true,
17-
// Prefer modern lifecycle events to avoid deprecated unload listeners.
18-
disablePageUnloadEvents: ['unload', 'beforeunload'],
19-
correlationHeaderDomains: [
20-
'writefluency.com',
21-
'api.writefluency.com',
22-
'writefluency.com:8080',
23-
'localhost:5000',
24-
],
25-
extensions: [this.angularPlugin],
26-
extensionConfig: {
27-
[this.angularPlugin.identifier]: {
28-
router: this.router,
29-
errorServices: [new ErrorHandler()],
30-
},
31-
},
32-
},
33-
});
34-
15+
private initializePromise: Promise<void> | null = null;
16+
private appInsights: MinimalAppInsights | null = null;
17+
3518
constructor(private router: Router) {
3619
if (typeof window === 'undefined' || !environment.production) {
3720
return;
3821
}
3922

4023
// Defer telemetry startup so it does not compete with LCP-critical resources.
4124
window.setTimeout(() => {
42-
this.initializeTelemetry();
25+
void this.initializeTelemetry().catch(() => undefined);
4326
}, 4000);
4427
}
4528

46-
private initializeTelemetry(): void {
47-
if (typeof window === 'undefined' || !environment.production) {
48-
return;
29+
private initializeTelemetry(): Promise<void> {
30+
if (typeof window === 'undefined' || !environment.production || this.initialized) {
31+
return Promise.resolve();
4932
}
5033

51-
if (this.initialized) {
52-
return;
34+
if (!this.initializePromise) {
35+
this.initializePromise = this.loadAndStartTelemetry().catch((error) => {
36+
this.initializePromise = null;
37+
throw error;
38+
});
5339
}
5440

55-
this.initialized = true;
41+
return this.initializePromise;
42+
}
43+
44+
private async loadAndStartTelemetry(): Promise<void> {
45+
const [{ AngularPlugin }, { ApplicationInsights, DistributedTracingModes }] = await Promise.all([
46+
import('@microsoft/applicationinsights-angularplugin-js'),
47+
import('@microsoft/applicationinsights-web'),
48+
]);
49+
50+
const angularPlugin = new AngularPlugin();
51+
const appInsights = new ApplicationInsights({
52+
config: {
53+
instrumentationKey: environment.instrumentationKey,
54+
distributedTracingMode: DistributedTracingModes.AI_AND_W3C,
55+
enableAutoRouteTracking: true,
56+
enableCorsCorrelation: true,
57+
// Prefer modern lifecycle events to avoid deprecated unload listeners.
58+
disablePageUnloadEvents: ['unload', 'beforeunload'],
59+
correlationHeaderDomains: [
60+
'writefluency.com',
61+
'api.writefluency.com',
62+
'writefluency.com:8080',
63+
'localhost:5000',
64+
],
65+
extensions: [angularPlugin],
66+
extensionConfig: {
67+
[angularPlugin.identifier]: {
68+
router: this.router,
69+
errorServices: [new ErrorHandler()],
70+
},
71+
},
72+
},
73+
});
5674

57-
this.appInsights.addTelemetryInitializer((envelope) => {
58-
if(!envelope.tags) {
59-
envelope.tags = {};
60-
}
61-
envelope.tags['ai.cloud.role'] = 'wf-webapp';
62-
envelope.tags['ai.cloud.roleInstance'] = 'angular-client';
75+
appInsights.addTelemetryInitializer((envelope: { tags?: Record<string, string> }) => {
76+
envelope.tags ??= {};
77+
envelope.tags['ai.cloud.role'] = 'wf-webapp';
78+
envelope.tags['ai.cloud.roleInstance'] = 'angular-client';
6379
});
6480

65-
this.appInsights.loadAppInsights();
81+
appInsights.loadAppInsights();
82+
this.appInsights = appInsights as MinimalAppInsights;
83+
this.initialized = true;
6684
}
6785

6886
// expose methods that can be used in components and services
6987
trackEvent(name: string): void {
70-
this.initializeTelemetry();
71-
this.appInsights.trackEvent({ name });
88+
if (!environment.production) {
89+
return;
90+
}
91+
92+
if (this.appInsights) {
93+
this.appInsights.trackEvent({ name });
94+
return;
95+
}
96+
97+
void this.initializeTelemetry()
98+
.then(() => this.appInsights?.trackEvent({ name }))
99+
.catch(() => undefined);
72100
}
73101

74102
trackTrace(message: string): void {
75-
this.initializeTelemetry();
76-
this.appInsights.trackTrace({ message });
103+
if (!environment.production) {
104+
return;
105+
}
106+
107+
if (this.appInsights) {
108+
this.appInsights.trackTrace({ message });
109+
return;
110+
}
111+
112+
void this.initializeTelemetry()
113+
.then(() => this.appInsights?.trackTrace({ message }))
114+
.catch(() => undefined);
77115
}
78116
}

0 commit comments

Comments
 (0)