Skip to content

Commit

Permalink
feat: Fixes JS Render Blocking (#34)
Browse files Browse the repository at this point in the history
* loading js script dynamically to avoid render blocking behavior

* adding checks for widget/player instances before trying to load the script, adding dev mode warnings
  • Loading branch information
colbyfayock authored Oct 1, 2024
1 parent 5d6fa5a commit eb3a8ff
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 153 deletions.
179 changes: 101 additions & 78 deletions astro-cloudinary/src/components/CldUploadWidget.astro
Original file line number Diff line number Diff line change
Expand Up @@ -44,93 +44,116 @@ if ( typeof className === 'string' ) {
<slot />
</span>

<script is:inline src="https://upload-widget.cloudinary.com/global/all.js" />

<script>
import type { CloudinaryCreateUploadWidget, CloudinaryUploadWidget } from '@cloudinary-util/types';
import { generateSignatureCallback, generateUploadWidgetResultCallback } from "@cloudinary-util/url-loader";
import { triggerOnIdle } from '../lib/util';
import { triggerOnIdle, loadScript } from '../lib/util';

interface Cloudinary {
createUploadWidget: CloudinaryCreateUploadWidget;
}

window.addEventListener('load', () => {
if ( 'cloudinary' in window ) {
const cloudinary = window.cloudinary as Cloudinary;
const widgets = document.querySelectorAll('.astro-cloudinary-clduploadwidget') as NodeListOf<HTMLSpanElement>;

widgets.forEach(widget => {
let widgetInstance: CloudinaryUploadWidget;

// Primary user interaction point for the upload widget, create
// an instance if it doesnt already exist (someone triggering before idle)
// then attempt to open the upload widget

widget.querySelector('button')?.addEventListener('click', (e) => {
if (!widgetInstance) {
widgetInstance = createWidget();
}
widgetInstance.open();
});

// Parse the upload options from the DOM and configure the
// remaining options that couldn't be serialized

const uploadOptions = widget.dataset.clduploadwidgetuploadOptions && JSON.parse(widget.dataset.clduploadwidgetuploadOptions);
const signatureEndpoint = widget.dataset.clduploadwidgetuploadSignatureendpoint;

const uploadSignature = signatureEndpoint && generateSignatureCallback({
signatureEndpoint: String(signatureEndpoint),
fetch
});

const resultsCallback = generateUploadWidgetResultCallback({
onError: (uploadError) => {
const customEvent = new CustomEvent(`clduploadwidget:error`, {
detail: {
event: 'error',
error: uploadError,
UploadWidget: widget,
},
});

widget?.dispatchEvent(customEvent);
},
onResult: (uploadResult) => {
if ( typeof uploadResult?.event !== 'string' ) return;

const customEvent = new CustomEvent(`clduploadwidget:${uploadResult?.event}`, {
detail: {
event: uploadResult.event,
info: uploadResult.info,
UploadWidget: widgetInstance,
},
});

widget?.dispatchEvent(customEvent);
},
});

// To help improve performance, attempt to create a new widget instance
// ONLY when the page is deemed idle. This helps offload the JS and
// additional file downloads for initialization until after the page
// settles while attempting to perform those functions before
// user interaction

triggerOnIdle(() => {
if ( !widgetInstance ) {
widgetInstance = createWidget();
}
});

function createWidget() {
return cloudinary.createUploadWidget({
...uploadOptions,
uploadSignature
}, resultsCallback);
window.addEventListener('load', async () => {
const widgets = document.querySelectorAll('.astro-cloudinary-clduploadwidget') as NodeListOf<HTMLSpanElement>;

if ( widgets.length === 0 ) return;

// Verify that the script already hasn't been loaded before trying to load it again

if ( !('cloudinary' in window) || typeof (window.cloudinary as Cloudinary).createUploadWidget !== 'function' ) {
await loadScript('https://upload-widget.cloudinary.com/global/all.js');
}

if ( !('cloudinary' in window) ) {
if ( import.meta.env.MODE === 'development' ) {
throw new Error('Unable to find cloudinary when loading the CldUploadWidget.')
}
// Silently exit if it can' tbe found, we don't want to break people's page in production just
// because the UW wouldn't load
return;
}

const cloudinary = window.cloudinary as Cloudinary;

widgets.forEach(widget => {
let widgetInstance: CloudinaryUploadWidget;

// Primary user interaction point for the upload widget, create
// an instance if it doesnt already exist (someone triggering before idle)
// then attempt to open the upload widget

const widgetButton = widget.querySelector('button');

if ( !widgetButton ) {
if ( import.meta.env.MODE === 'development' ) {
throw new Error('Unable to find button element in CldUploadWidget. Please nest a button inside. (Ex: <CldUploadWidget><button /></CldUploadWidget>)')
}
return;
}

widgetButton.addEventListener('click', (e) => {
if (!widgetInstance) {
widgetInstance = createWidget();
}
widgetInstance.open();
});
}

// Parse the upload options from the DOM and configure the
// remaining options that couldn't be serialized

const uploadOptions = widget.dataset.clduploadwidgetuploadOptions && JSON.parse(widget.dataset.clduploadwidgetuploadOptions);
const signatureEndpoint = widget.dataset.clduploadwidgetuploadSignatureendpoint;

const uploadSignature = signatureEndpoint && generateSignatureCallback({
signatureEndpoint: String(signatureEndpoint),
fetch
});

const resultsCallback = generateUploadWidgetResultCallback({
onError: (uploadError) => {
const customEvent = new CustomEvent(`clduploadwidget:error`, {
detail: {
event: 'error',
error: uploadError,
UploadWidget: widget,
},
});

widget?.dispatchEvent(customEvent);
},
onResult: (uploadResult) => {
if ( typeof uploadResult?.event !== 'string' ) return;

const customEvent = new CustomEvent(`clduploadwidget:${uploadResult?.event}`, {
detail: {
event: uploadResult.event,
info: uploadResult.info,
UploadWidget: widgetInstance,
},
});

widget?.dispatchEvent(customEvent);
},
});

// To help improve performance, attempt to create a new widget instance
// ONLY when the page is deemed idle. This helps offload the JS and
// additional file downloads for initialization until after the page
// settles while attempting to perform those functions before
// user interaction

triggerOnIdle(() => {
if ( !widgetInstance ) {
widgetInstance = createWidget();
}
});

function createWidget() {
return cloudinary.createUploadWidget({
...uploadOptions,
uploadSignature
}, resultsCallback);
}
});
})
</script>
169 changes: 94 additions & 75 deletions astro-cloudinary/src/components/CldVideoPlayer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { CloudinaryVideoPlayerOptions, CloudinaryVideoPlayerOptionsLogo } f
import type { GetCldImageUrlOptions } from "../helpers/getCldImageUrl";
import type { GetCldVideoUrlOptions } from "../helpers/getCldVideoUrl";
const PLAYER_VERSION = '2.0.5';
const PLAYER_VERSION = '2.1.0';
export interface CldVideoPlayerProps extends Omit<CloudinaryVideoPlayerOptions, "autoplayMode" | "cloud_name" | "controlBar" | "height" | "logoImageUrl" | "logoOnclickUrl" | "posterOptions" | "publicId" | "secure" | "showLogo" | "width"> {
class?: string;
Expand Down Expand Up @@ -73,82 +73,101 @@ if ( className ) {
/>
</div>

<script is:inline src={`https://unpkg.com/cloudinary-video-player@${PLAYER_VERSION}/dist/cld-video-player.min.js`}></script>

<script>
import type { CloudinaryVideoPlayer } from '@cloudinary-util/types';

interface Cloudinary {
videoPlayer: (video: HTMLVideoElement, options: {}) => CloudinaryVideoPlayer;
import type { CloudinaryVideoPlayer } from '@cloudinary-util/types';
import { loadScript } from '../lib/util';

interface Cloudinary {
videoPlayer: (video: HTMLVideoElement, options: {}) => CloudinaryVideoPlayer;
}

window.addEventListener('load', async () => {
const videos = document.querySelectorAll('.astro-cloudinary-cldvideoplayer') as NodeListOf<HTMLVideoElement>;

if ( videos.length === 0 ) return;

// Verify that the script already hasn't been loaded before trying to load it again

if ( !('cloudinary' in window) || typeof (window.cloudinary as Cloudinary).videoPlayer !== 'function' ) {
await loadScript('https://unpkg.com/[email protected]/dist/cld-video-player.min.js');
}

window.addEventListener('load', () => {
if ( 'cloudinary' in window ) {
const cloudinary = window.cloudinary as Cloudinary;
const videos = document.querySelectorAll('.astro-cloudinary-cldvideoplayer') as NodeListOf<HTMLVideoElement>;

if ( import.meta.env.MODE === 'development' ) {
const playerIds = Array.from(videos).map(video => video.dataset.cldvideoplayerId);
if ( new Set(playerIds).size !== playerIds.length ) {
console.warn('[CldVideoPlayer] Multiple instances of the same video detected on the page which may cause unexpected results. Try adding a unique id to each player.');
}
}

videos.forEach(video => {
const playerOptions = video.dataset.cldvideoplayerOptions && JSON.parse(video.dataset.cldvideoplayerOptions);
const player = cloudinary.videoPlayer(video, playerOptions);

if ( !player ) return;

// Loop through all avialable player events and create custom event callbacks

const events = [
'loadstart',
'suspend',
'abort',
'error',
'emptied',
'stalled',
'loadedmetadata',
'loadeddata',
'canplay',
'canplaythrough',
'playing',
'waiting',
'seeking',
'seeked',
'ended',
'durationchange',
'timeupdate',
'progress',
'play',
'pause',
'ratechange',
'volumechange',
'fullscreenchange',
'posterchange',
'mute',
'unmute',
'percentsplayed',
'timeplayed',
'seek',
'sourcechanged',
'qualitychanged',
];

events.forEach(event => {
player.on(event, (e: { Player: CloudinaryVideoPlayer; type: string; }) => {
const customEvent = new CustomEvent(`cldvideoplayer:${event}`, {
detail: {
Player: e.Player,
type: e.type,
Video: video
},
});
video.closest(`#${video.dataset.cldvideoplayerId}`)?.dispatchEvent(customEvent);
});
});
})
if ( !('cloudinary' in window) ) {
if ( import.meta.env.MODE === 'development' ) {
throw new Error('Unable to find cloudinary when loading the CldVideoPlayer.')
}
});
// Silently exit if it can' tbe found, we don't want to break people's page in production just
// because the player wouldn't load
return;
}

const cloudinary = window.cloudinary as Cloudinary;

// Check to see if there are any duplicate player IDs
// This should be rare but if there are duplicates it can create conflicts
// in how the player loads with the given settings

if ( import.meta.env.MODE === 'development' ) {
const playerIds = Array.from(videos).map(video => video.dataset.cldvideoplayerId);
if ( new Set(playerIds).size !== playerIds.length ) {
console.warn('[CldVideoPlayer] Multiple instances of the same video detected on the page which may cause unexpected results. Try adding a unique id to each player.');
}
}

videos.forEach(video => {
const playerOptions = video.dataset.cldvideoplayerOptions && JSON.parse(video.dataset.cldvideoplayerOptions);
const player = cloudinary.videoPlayer(video, playerOptions);

if ( !player ) return;

// Loop through all avialable player events and create custom event callbacks

const events = [
'loadstart',
'suspend',
'abort',
'error',
'emptied',
'stalled',
'loadedmetadata',
'loadeddata',
'canplay',
'canplaythrough',
'playing',
'waiting',
'seeking',
'seeked',
'ended',
'durationchange',
'timeupdate',
'progress',
'play',
'pause',
'ratechange',
'volumechange',
'fullscreenchange',
'posterchange',
'mute',
'unmute',
'percentsplayed',
'timeplayed',
'seek',
'sourcechanged',
'qualitychanged',
];

events.forEach(event => {
player.on(event, (e: { Player: CloudinaryVideoPlayer; type: string; }) => {
const customEvent = new CustomEvent(`cldvideoplayer:${event}`, {
detail: {
Player: e.Player,
type: e.type,
Video: video
},
});
video.closest(`#${video.dataset.cldvideoplayerId}`)?.dispatchEvent(customEvent);
});
});
})
});
</script>
Loading

0 comments on commit eb3a8ff

Please sign in to comment.