Skip to content

Commit b7c47c3

Browse files
authored
feat(youtube-player): improve initial load performance using a placeholder image (#28207)
Currently the `youtube-player` component loads the YouTube API and sets up the video on initialization. This can slow the page down a lot, because it loads and executes ~150kb of JavaScript, even though the video isn't playing. These changes rework the `youtube-player` component to show the thumbnail of the video and a fake button instead. When the button is clicked, the API will be loaded and the video will be autoplayed, thus moving the YouTube API out of the critical path. There are a few cases where the placeholder won't be shown: * A video that plays automatically. * When the `youtube-player` is showing a playlist, rather than a single video.
1 parent 6b2f03b commit b7c47c3

File tree

11 files changed

+632
-88
lines changed

11 files changed

+632
-88
lines changed

.stylelintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136
],
137137
"linebreaks": "unix",
138138
"selector-class-pattern": [
139-
"^_?(mat-|cdk-|example-|demo-|ng-|mdc-|map-|test-)",
139+
"^_?(mat-|cdk-|example-|demo-|ng-|mdc-|map-|test-|youtube-player-)",
140140
{
141141
"resolveNestedSelectors": true
142142
}

src/dev-app/youtube-player/youtube-player-demo.html

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div #demoYouTubePlayer class="demo-youtube-player">
2-
<h1>Basic Example</h1>
2+
<h2>Basic Example</h2>
33
<section>
44
<div class="demo-video-selection">
55
<label>Pick the video:</label>
@@ -12,10 +12,31 @@ <h1>Basic Example</h1>
1212
</div>
1313
<div class="demo-video-selection">
1414
<mat-checkbox [(ngModel)]="disableCookies">Disable cookies</mat-checkbox>
15+
<mat-checkbox [(ngModel)]="disablePlaceholder">Disable placeholder</mat-checkbox>
1516
</div>
1617
<youtube-player [videoId]="selectedVideoId"
1718
[playerVars]="playerVars"
18-
[width]="videoWidth" [height]="videoHeight"
19-
[disableCookies]="disableCookies"></youtube-player>
19+
[width]="videoWidth"
20+
[height]="videoHeight"
21+
[disableCookies]="disableCookies"
22+
[disablePlaceholder]="disablePlaceholder"
23+
[placeholderImageQuality]="placeholderQuality"></youtube-player>
2024
</section>
25+
26+
<h2>Placeholder quality comparison (high to low)</h2>
27+
<youtube-player
28+
[videoId]="selectedVideoId"
29+
[width]="videoWidth"
30+
[height]="videoHeight"
31+
placeholderImageQuality="high"/>
32+
<youtube-player
33+
[videoId]="selectedVideoId"
34+
[width]="videoWidth"
35+
[height]="videoHeight"
36+
placeholderImageQuality="standard"/>
37+
<youtube-player
38+
[videoId]="selectedVideoId"
39+
[width]="videoWidth"
40+
[height]="videoHeight"
41+
placeholderImageQuality="low"/>
2142
</div>

src/dev-app/youtube-player/youtube-player-demo.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,60 @@ import {
1616
} from '@angular/core';
1717
import {FormsModule} from '@angular/forms';
1818
import {MatRadioModule} from '@angular/material/radio';
19-
import {YouTubePlayer} from '@angular/youtube-player';
19+
import {PlaceholderImageQuality, YouTubePlayer} from '@angular/youtube-player';
2020
import {MatCheckboxModule} from '@angular/material/checkbox';
2121

2222
interface Video {
2323
id: string;
2424
name: string;
2525
isPlaylist?: boolean;
26+
autoplay?: boolean;
27+
placeholderQuality: PlaceholderImageQuality;
2628
}
2729

2830
const VIDEOS: Video[] = [
2931
{
30-
id: 'PRQCAL_RMVo',
31-
name: 'Instructional',
32+
id: 'hsUxJjY-PRg',
33+
name: 'Control Flow',
34+
placeholderQuality: 'high',
3235
},
3336
{
3437
id: 'O0xx5SvjmnU',
3538
name: 'Angular Conf',
39+
placeholderQuality: 'high',
3640
},
3741
{
3842
id: 'invalidname',
3943
name: 'Invalid',
44+
placeholderQuality: 'high',
4045
},
4146
{
4247
id: 'PLOa5YIicjJ-XCGXwnEmMmpHHCn11gUgvL',
4348
name: 'Angular Forms Playlist',
4449
isPlaylist: true,
50+
placeholderQuality: 'high',
4551
},
4652
{
4753
id: 'PLOa5YIicjJ-VpOOoLczAGTLEEznZ2JEa6',
4854
name: 'Angular Router Playlist',
4955
isPlaylist: true,
56+
placeholderQuality: 'high',
57+
},
58+
{
59+
id: 'PXNp4LENMPA',
60+
name: 'Angular.dev (autoplay)',
61+
autoplay: true,
62+
placeholderQuality: 'high',
63+
},
64+
{
65+
id: 'txqiwrbYGrs',
66+
name: 'David after dentist (only standard quality placeholder)',
67+
placeholderQuality: 'low',
68+
},
69+
{
70+
id: 'EwTZ2xpQwpA',
71+
name: 'Chocolate rain (only low quality placeholder)',
72+
placeholderQuality: 'low',
5073
},
5174
];
5275

@@ -67,6 +90,8 @@ export class YouTubePlayerDemo implements AfterViewInit, OnDestroy {
6790
videoWidth: number | undefined;
6891
videoHeight: number | undefined;
6992
disableCookies = false;
93+
disablePlaceholder = false;
94+
placeholderQuality: PlaceholderImageQuality;
7095

7196
constructor(private _changeDetectorRef: ChangeDetectorRef) {
7297
this.selectedVideo = VIDEOS[0];
@@ -102,11 +127,12 @@ export class YouTubePlayerDemo implements AfterViewInit, OnDestroy {
102127

103128
set selectedVideo(value: Video | undefined) {
104129
this._selectedVideo = value;
130+
this.placeholderQuality = value?.placeholderQuality || 'standard';
105131

106132
// If the video is a playlist, don't send a video id, and prepare playerVars instead
107133

108134
if (!value?.isPlaylist) {
109-
this._playerVars = undefined;
135+
this._playerVars = value?.autoplay ? {autoplay: 1} : undefined;
110136
this._selectedVideoId = value?.id;
111137
return;
112138
}

src/youtube-player/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ load(
44
"ng_package",
55
"ng_test_library",
66
"ng_web_test_suite",
7+
"sass_binary",
78
)
89

910
package(default_visibility = ["//visibility:public"])
@@ -17,6 +18,9 @@ ng_module(
1718
"fake-youtube-player.ts",
1819
],
1920
),
21+
assets = [
22+
":youtube_player_placeholder_scss",
23+
],
2024
deps = [
2125
"//src:dev_mode_types",
2226
"@npm//@angular/common",
@@ -26,6 +30,11 @@ ng_module(
2630
],
2731
)
2832

33+
sass_binary(
34+
name = "youtube_player_placeholder_scss",
35+
src = "youtube-player-placeholder.scss",
36+
)
37+
2938
ng_package(
3039
name = "npm_package",
3140
srcs = ["package.json"],

src/youtube-player/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,74 @@ import {YouTubePlayer, YOUTUBE_PLAYER_CONFIG} from '@angular/youtube-player';
5858
})
5959
export class YourApp {}
6060
```
61+
62+
## Loading behavior
63+
By default the `<youtube-player/>` will show a placeholder element instead of loading the API
64+
up-front until the user interacts with it. This speeds up the initial render of the page by not
65+
loading unnecessary JavaScript for a video that might not be played. Once the user clicks on the
66+
video, the API will be loaded and the placeholder will be swapped out with the actual video.
67+
68+
Note that the placeholder won't be shown in the following scenarios:
69+
* Video that plays automatically (e.g. `playerVars` contains `autoplay: 1`).
70+
* The player is showing a playlist (e.g. `playerVars` contains a `list` property).
71+
72+
If you want to disable the placeholder and have the `<youtube-player/>` load the API on
73+
initialization, you can either pass in the `disablePlaceholder` input:
74+
75+
```html
76+
<youtube-player videoId="mVjYG9TSN88" disablePlaceholder/>
77+
```
78+
79+
Or set it at a global level using the `YOUTUBE_PLAYER_CONFIG` injection token:
80+
81+
```typescript
82+
import {NgModule} from '@angular/core';
83+
import {YouTubePlayer, YOUTUBE_PLAYER_CONFIG} from '@angular/youtube-player';
84+
85+
@NgModule({
86+
imports: [YouTubePlayer],
87+
providers: [{
88+
provide: YOUTUBE_PLAYER_CONFIG,
89+
useValue: {
90+
disablePlaceholder: true
91+
}
92+
}]
93+
})
94+
export class YourApp {}
95+
```
96+
97+
### Placeholder image quality
98+
YouTube provides different sizes of placeholder images depending on when the video was uploaded
99+
and the thumbnail that was provided by the uploader. The `<youtube-player/>` defaults to a quality
100+
that should be available for the majority of videos, but if you're seeing a grey placeholder,
101+
consider switching to the `low` quality using the `placeholderImageQuality` input or through the
102+
`YOUTUBE_PLAYER_CONFIG`.
103+
104+
```html
105+
<!-- Default value, should exist for most videos. -->
106+
<youtube-player videoId="mVjYG9TSN88" placeholderImageQuality="standard"/>
107+
108+
<!-- High quality image that should be present for most videos from the past few years. -->
109+
<youtube-player videoId="mVjYG9TSN88" placeholderImageQuality="high"/>
110+
111+
<!-- Very low quality image, but should exist for all videos. -->
112+
<youtube-player videoId="mVjYG9TSN88" placeholderImageQuality="low"/>
113+
```
114+
115+
### Placeholder internationalization
116+
Since the placeholder has an interactive `button` element, it needs an `aria-label` for proper
117+
accessibility. The default label is "Play video", but you can customize it based on your app through
118+
the `placeholderButtonLabel` input or the `YOUTUBE_PLAYER_CONFIG` injection token:
119+
120+
```html
121+
<youtube-player videoId="mVjYG9TSN88" placeholderButtonLabel="Afspil video"/>
122+
```
123+
124+
### Placeholder caveats
125+
There are a couple of considerations when using placeholders:
126+
1. Different videos support different sizes of placeholder images and there's no way to know
127+
ahead of time which one is supported. The `<youtube-player/>` defaults to a value that should
128+
work for most videos, but if you want something higher or lower, you can refer to the
129+
["Placeholder image quality" section](#placeholder-image-quality).
130+
2. Unlike the native YouTube placeholder, the Angular component doesn't show the video's title,
131+
because it isn't known ahead of time.

src/youtube-player/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88

99
export * from './youtube-module';
1010
export {YouTubePlayer, YOUTUBE_PLAYER_CONFIG, YouTubePlayerConfig} from './youtube-player';
11+
export {PlaceholderImageQuality} from './youtube-player-placeholder';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.youtube-player-placeholder {
2+
display: flex;
3+
align-items: center;
4+
justify-content: center;
5+
width: 100%;
6+
overflow: hidden;
7+
cursor: pointer;
8+
background-color: #000;
9+
background-position: center center;
10+
background-size: cover;
11+
transition: box-shadow 300ms ease;
12+
13+
// YouTube has a slight drop shadow on the preview that we try to imitate here.
14+
// Note that they use a base64 image, likely for performance reasons. We can't use the
15+
// image, because it can break users with a CSP that doesn't allow `data:` URLs.
16+
box-shadow: inset 0 120px 90px -90px rgba(0, 0, 0, 0.8);
17+
}
18+
19+
.youtube-player-placeholder-button {
20+
transition: opacity 300ms ease;
21+
-moz-appearance: none;
22+
-webkit-appearance: none;
23+
background: none;
24+
border: none;
25+
padding: 0;
26+
display: flex;
27+
28+
svg {
29+
width: 68px;
30+
height: 48px;
31+
}
32+
}
33+
34+
.youtube-player-placeholder-loading {
35+
box-shadow: none;
36+
37+
.youtube-player-placeholder-button {
38+
opacity: 0;
39+
}
40+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
10+
11+
/** Quality of the placeholder image. */
12+
export type PlaceholderImageQuality = 'high' | 'standard' | 'low';
13+
14+
@Component({
15+
selector: 'youtube-player-placeholder',
16+
changeDetection: ChangeDetectionStrategy.OnPush,
17+
encapsulation: ViewEncapsulation.None,
18+
template: `
19+
<button type="button" class="youtube-player-placeholder-button" [attr.aria-label]="buttonLabel">
20+
<svg
21+
height="100%"
22+
version="1.1"
23+
viewBox="0 0 68 48"
24+
focusable="false"
25+
aria-hidden="true">
26+
<path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00"></path>
27+
<path d="M 45,24 27,14 27,34" fill="#fff"></path>
28+
</svg>
29+
</button>
30+
`,
31+
standalone: true,
32+
styleUrl: 'youtube-player-placeholder.css',
33+
host: {
34+
'class': 'youtube-player-placeholder',
35+
'[class.youtube-player-placeholder-loading]': 'isLoading',
36+
'[style.background-image]': '_getBackgroundImage()',
37+
'[style.width.px]': 'width',
38+
'[style.height.px]': 'height',
39+
},
40+
})
41+
export class YouTubePlayerPlaceholder {
42+
/** ID of the video for which to show the placeholder. */
43+
@Input() videoId: string;
44+
45+
/** Width of the video for which to show the placeholder. */
46+
@Input() width: number;
47+
48+
/** Height of the video for which to show the placeholder. */
49+
@Input() height: number;
50+
51+
/** Whether the video is currently being loaded. */
52+
@Input() isLoading: boolean;
53+
54+
/** Accessible label for the play button. */
55+
@Input() buttonLabel: string;
56+
57+
/** Quality of the placeholder image. */
58+
@Input() quality: PlaceholderImageQuality;
59+
60+
/** Gets the background image showing the placeholder. */
61+
protected _getBackgroundImage(): string | undefined {
62+
let url: string;
63+
64+
if (this.quality === 'low') {
65+
url = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;
66+
} else if (this.quality === 'high') {
67+
url = `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg`;
68+
} else {
69+
url = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
70+
}
71+
72+
return `url(${url})`;
73+
}
74+
}

0 commit comments

Comments
 (0)