Skip to content

Commit

Permalink
feat(youtube-player): improve initial load performance using a placeh…
Browse files Browse the repository at this point in the history
…older 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.
  • Loading branch information
crisbeto authored Nov 30, 2023
1 parent 6b2f03b commit b7c47c3
Show file tree
Hide file tree
Showing 11 changed files with 632 additions and 88 deletions.
2 changes: 1 addition & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
],
"linebreaks": "unix",
"selector-class-pattern": [
"^_?(mat-|cdk-|example-|demo-|ng-|mdc-|map-|test-)",
"^_?(mat-|cdk-|example-|demo-|ng-|mdc-|map-|test-|youtube-player-)",
{
"resolveNestedSelectors": true
}
Expand Down
27 changes: 24 additions & 3 deletions src/dev-app/youtube-player/youtube-player-demo.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div #demoYouTubePlayer class="demo-youtube-player">
<h1>Basic Example</h1>
<h2>Basic Example</h2>
<section>
<div class="demo-video-selection">
<label>Pick the video:</label>
Expand All @@ -12,10 +12,31 @@ <h1>Basic Example</h1>
</div>
<div class="demo-video-selection">
<mat-checkbox [(ngModel)]="disableCookies">Disable cookies</mat-checkbox>
<mat-checkbox [(ngModel)]="disablePlaceholder">Disable placeholder</mat-checkbox>
</div>
<youtube-player [videoId]="selectedVideoId"
[playerVars]="playerVars"
[width]="videoWidth" [height]="videoHeight"
[disableCookies]="disableCookies"></youtube-player>
[width]="videoWidth"
[height]="videoHeight"
[disableCookies]="disableCookies"
[disablePlaceholder]="disablePlaceholder"
[placeholderImageQuality]="placeholderQuality"></youtube-player>
</section>

<h2>Placeholder quality comparison (high to low)</h2>
<youtube-player
[videoId]="selectedVideoId"
[width]="videoWidth"
[height]="videoHeight"
placeholderImageQuality="high"/>
<youtube-player
[videoId]="selectedVideoId"
[width]="videoWidth"
[height]="videoHeight"
placeholderImageQuality="standard"/>
<youtube-player
[videoId]="selectedVideoId"
[width]="videoWidth"
[height]="videoHeight"
placeholderImageQuality="low"/>
</div>
34 changes: 30 additions & 4 deletions src/dev-app/youtube-player/youtube-player-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,60 @@ import {
} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatRadioModule} from '@angular/material/radio';
import {YouTubePlayer} from '@angular/youtube-player';
import {PlaceholderImageQuality, YouTubePlayer} from '@angular/youtube-player';
import {MatCheckboxModule} from '@angular/material/checkbox';

interface Video {
id: string;
name: string;
isPlaylist?: boolean;
autoplay?: boolean;
placeholderQuality: PlaceholderImageQuality;
}

const VIDEOS: Video[] = [
{
id: 'PRQCAL_RMVo',
name: 'Instructional',
id: 'hsUxJjY-PRg',
name: 'Control Flow',
placeholderQuality: 'high',
},
{
id: 'O0xx5SvjmnU',
name: 'Angular Conf',
placeholderQuality: 'high',
},
{
id: 'invalidname',
name: 'Invalid',
placeholderQuality: 'high',
},
{
id: 'PLOa5YIicjJ-XCGXwnEmMmpHHCn11gUgvL',
name: 'Angular Forms Playlist',
isPlaylist: true,
placeholderQuality: 'high',
},
{
id: 'PLOa5YIicjJ-VpOOoLczAGTLEEznZ2JEa6',
name: 'Angular Router Playlist',
isPlaylist: true,
placeholderQuality: 'high',
},
{
id: 'PXNp4LENMPA',
name: 'Angular.dev (autoplay)',
autoplay: true,
placeholderQuality: 'high',
},
{
id: 'txqiwrbYGrs',
name: 'David after dentist (only standard quality placeholder)',
placeholderQuality: 'low',
},
{
id: 'EwTZ2xpQwpA',
name: 'Chocolate rain (only low quality placeholder)',
placeholderQuality: 'low',
},
];

Expand All @@ -67,6 +90,8 @@ export class YouTubePlayerDemo implements AfterViewInit, OnDestroy {
videoWidth: number | undefined;
videoHeight: number | undefined;
disableCookies = false;
disablePlaceholder = false;
placeholderQuality: PlaceholderImageQuality;

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

set selectedVideo(value: Video | undefined) {
this._selectedVideo = value;
this.placeholderQuality = value?.placeholderQuality || 'standard';

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

if (!value?.isPlaylist) {
this._playerVars = undefined;
this._playerVars = value?.autoplay ? {autoplay: 1} : undefined;
this._selectedVideoId = value?.id;
return;
}
Expand Down
9 changes: 9 additions & 0 deletions src/youtube-player/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load(
"ng_package",
"ng_test_library",
"ng_web_test_suite",
"sass_binary",
)

package(default_visibility = ["//visibility:public"])
Expand All @@ -17,6 +18,9 @@ ng_module(
"fake-youtube-player.ts",
],
),
assets = [
":youtube_player_placeholder_scss",
],
deps = [
"//src:dev_mode_types",
"@npm//@angular/common",
Expand All @@ -26,6 +30,11 @@ ng_module(
],
)

sass_binary(
name = "youtube_player_placeholder_scss",
src = "youtube-player-placeholder.scss",
)

ng_package(
name = "npm_package",
srcs = ["package.json"],
Expand Down
71 changes: 71 additions & 0 deletions src/youtube-player/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,74 @@ import {YouTubePlayer, YOUTUBE_PLAYER_CONFIG} from '@angular/youtube-player';
})
export class YourApp {}
```

## Loading behavior
By default the `<youtube-player/>` will show a placeholder element instead of loading the API
up-front until the user interacts with it. This speeds up the initial render of the page by not
loading unnecessary JavaScript for a video that might not be played. Once the user clicks on the
video, the API will be loaded and the placeholder will be swapped out with the actual video.

Note that the placeholder won't be shown in the following scenarios:
* Video that plays automatically (e.g. `playerVars` contains `autoplay: 1`).
* The player is showing a playlist (e.g. `playerVars` contains a `list` property).

If you want to disable the placeholder and have the `<youtube-player/>` load the API on
initialization, you can either pass in the `disablePlaceholder` input:

```html
<youtube-player videoId="mVjYG9TSN88" disablePlaceholder/>
```

Or set it at a global level using the `YOUTUBE_PLAYER_CONFIG` injection token:

```typescript
import {NgModule} from '@angular/core';
import {YouTubePlayer, YOUTUBE_PLAYER_CONFIG} from '@angular/youtube-player';

@NgModule({
imports: [YouTubePlayer],
providers: [{
provide: YOUTUBE_PLAYER_CONFIG,
useValue: {
disablePlaceholder: true
}
}]
})
export class YourApp {}
```

### Placeholder image quality
YouTube provides different sizes of placeholder images depending on when the video was uploaded
and the thumbnail that was provided by the uploader. The `<youtube-player/>` defaults to a quality
that should be available for the majority of videos, but if you're seeing a grey placeholder,
consider switching to the `low` quality using the `placeholderImageQuality` input or through the
`YOUTUBE_PLAYER_CONFIG`.

```html
<!-- Default value, should exist for most videos. -->
<youtube-player videoId="mVjYG9TSN88" placeholderImageQuality="standard"/>

<!-- High quality image that should be present for most videos from the past few years. -->
<youtube-player videoId="mVjYG9TSN88" placeholderImageQuality="high"/>

<!-- Very low quality image, but should exist for all videos. -->
<youtube-player videoId="mVjYG9TSN88" placeholderImageQuality="low"/>
```

### Placeholder internationalization
Since the placeholder has an interactive `button` element, it needs an `aria-label` for proper
accessibility. The default label is "Play video", but you can customize it based on your app through
the `placeholderButtonLabel` input or the `YOUTUBE_PLAYER_CONFIG` injection token:

```html
<youtube-player videoId="mVjYG9TSN88" placeholderButtonLabel="Afspil video"/>
```

### Placeholder caveats
There are a couple of considerations when using placeholders:
1. Different videos support different sizes of placeholder images and there's no way to know
ahead of time which one is supported. The `<youtube-player/>` defaults to a value that should
work for most videos, but if you want something higher or lower, you can refer to the
["Placeholder image quality" section](#placeholder-image-quality).
2. Unlike the native YouTube placeholder, the Angular component doesn't show the video's title,
because it isn't known ahead of time.
1 change: 1 addition & 0 deletions src/youtube-player/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export * from './youtube-module';
export {YouTubePlayer, YOUTUBE_PLAYER_CONFIG, YouTubePlayerConfig} from './youtube-player';
export {PlaceholderImageQuality} from './youtube-player-placeholder';
40 changes: 40 additions & 0 deletions src/youtube-player/youtube-player-placeholder.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.youtube-player-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
overflow: hidden;
cursor: pointer;
background-color: #000;
background-position: center center;
background-size: cover;
transition: box-shadow 300ms ease;

// YouTube has a slight drop shadow on the preview that we try to imitate here.
// Note that they use a base64 image, likely for performance reasons. We can't use the
// image, because it can break users with a CSP that doesn't allow `data:` URLs.
box-shadow: inset 0 120px 90px -90px rgba(0, 0, 0, 0.8);
}

.youtube-player-placeholder-button {
transition: opacity 300ms ease;
-moz-appearance: none;
-webkit-appearance: none;
background: none;
border: none;
padding: 0;
display: flex;

svg {
width: 68px;
height: 48px;
}
}

.youtube-player-placeholder-loading {
box-shadow: none;

.youtube-player-placeholder-button {
opacity: 0;
}
}
74 changes: 74 additions & 0 deletions src/youtube-player/youtube-player-placeholder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';

/** Quality of the placeholder image. */
export type PlaceholderImageQuality = 'high' | 'standard' | 'low';

@Component({
selector: 'youtube-player-placeholder',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
template: `
<button type="button" class="youtube-player-placeholder-button" [attr.aria-label]="buttonLabel">
<svg
height="100%"
version="1.1"
viewBox="0 0 68 48"
focusable="false"
aria-hidden="true">
<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>
<path d="M 45,24 27,14 27,34" fill="#fff"></path>
</svg>
</button>
`,
standalone: true,
styleUrl: 'youtube-player-placeholder.css',
host: {
'class': 'youtube-player-placeholder',
'[class.youtube-player-placeholder-loading]': 'isLoading',
'[style.background-image]': '_getBackgroundImage()',
'[style.width.px]': 'width',
'[style.height.px]': 'height',
},
})
export class YouTubePlayerPlaceholder {
/** ID of the video for which to show the placeholder. */
@Input() videoId: string;

/** Width of the video for which to show the placeholder. */
@Input() width: number;

/** Height of the video for which to show the placeholder. */
@Input() height: number;

/** Whether the video is currently being loaded. */
@Input() isLoading: boolean;

/** Accessible label for the play button. */
@Input() buttonLabel: string;

/** Quality of the placeholder image. */
@Input() quality: PlaceholderImageQuality;

/** Gets the background image showing the placeholder. */
protected _getBackgroundImage(): string | undefined {
let url: string;

if (this.quality === 'low') {
url = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;
} else if (this.quality === 'high') {
url = `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg`;
} else {
url = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
}

return `url(${url})`;
}
}
Loading

0 comments on commit b7c47c3

Please sign in to comment.