1
0
mirror of https://github.com/videojs/video.js.git synced 2025-01-27 11:22:06 +02:00

perf: Various small performance improvements. (#4426)

These are a few performance improvements.

* Use textContent instead of innerHTML for text content.
* Construct TextTrackSettings content with HTML strings where possible because it's a bit faster than manual DOM construction.
* Do minimal DOM updates to time controls. This reduces our timeupdate footprint from ~2-2.5ms to ~1ms!
This commit is contained in:
Pat O'Neill 2017-06-28 02:32:58 -04:00 committed by Gary Katsevman
parent 7f7ea70cb7
commit 77ba3d13d9
6 changed files with 191 additions and 162 deletions

View File

@ -120,7 +120,7 @@ class ClickableComponent extends Component {
const localizedText = this.localize(text); const localizedText = this.localize(text);
this.controlText_ = text; this.controlText_ = text;
this.controlTextEl_.innerHTML = localizedText; Dom.textContent(this.controlTextEl_, localizedText);
if (!this.nonIconControl) { if (!this.nonIconControl) {
// Set title attribute if only an icon is shown // Set title attribute if only an icon is shown
el.setAttribute('title', localizedText); el.setAttribute('title', localizedText);

View File

@ -1,8 +1,10 @@
/** /**
* @file current-time-display.js * @file current-time-display.js
*/ */
import document from 'global/document';
import Component from '../../component.js'; import Component from '../../component.js';
import * as Dom from '../../utils/dom.js'; import * as Dom from '../../utils/dom.js';
import {bind, throttle} from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js'; import formatTime from '../../utils/format-time.js';
/** /**
@ -23,8 +25,8 @@ class CurrentTimeDisplay extends Component {
*/ */
constructor(player, options) { constructor(player, options) {
super(player, options); super(player, options);
this.throttledUpdateContent = throttle(bind(this, this.updateContent), 25);
this.on(player, 'timeupdate', this.updateContent); this.on(player, 'timeupdate', this.throttledUpdateContent);
} }
/** /**
@ -39,18 +41,34 @@ class CurrentTimeDisplay extends Component {
}); });
this.contentEl_ = Dom.createEl('div', { this.contentEl_ = Dom.createEl('div', {
className: 'vjs-current-time-display', className: 'vjs-current-time-display'
// label the current time for screen reader users
innerHTML: '<span class="vjs-control-text">Current Time </span>' + '0:00'
}, { }, {
// tell screen readers not to automatically read the time as it changes // tell screen readers not to automatically read the time as it changes
'aria-live': 'off' 'aria-live': 'off'
}); }, Dom.createEl('span', {
className: 'vjs-control-text',
textContent: this.localize('Current Time')
}));
this.updateTextNode_();
el.appendChild(this.contentEl_); el.appendChild(this.contentEl_);
return el; return el;
} }
/**
* Updates the "current time" text node with new content using the
* contents of the `formattedTime_` property.
*
* @private
*/
updateTextNode_() {
if (this.textNode_) {
this.contentEl_.removeChild(this.textNode_);
}
this.textNode_ = document.createTextNode(` ${this.formattedTime_ || '0:00'}`);
this.contentEl_.appendChild(this.textNode_);
}
/** /**
* Update current time display * Update current time display
* *
@ -62,12 +80,11 @@ class CurrentTimeDisplay extends Component {
updateContent(event) { updateContent(event) {
// Allows for smooth scrubbing, when player can't keep up. // Allows for smooth scrubbing, when player can't keep up.
const time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); const time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime();
const localizedText = this.localize('Current Time');
const formattedTime = formatTime(time, this.player_.duration()); const formattedTime = formatTime(time, this.player_.duration());
if (formattedTime !== this.formattedTime_) { if (formattedTime !== this.formattedTime_) {
this.formattedTime_ = formattedTime; this.formattedTime_ = formattedTime;
this.contentEl_.innerHTML = `<span class="vjs-control-text">${localizedText}</span> ${formattedTime}`; this.requestAnimationFrame(this.updateTextNode_);
} }
} }

View File

@ -1,8 +1,10 @@
/** /**
* @file duration-display.js * @file duration-display.js
*/ */
import document from 'global/document';
import Component from '../../component.js'; import Component from '../../component.js';
import * as Dom from '../../utils/dom.js'; import * as Dom from '../../utils/dom.js';
import {bind, throttle} from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js'; import formatTime from '../../utils/format-time.js';
/** /**
@ -24,13 +26,17 @@ class DurationDisplay extends Component {
constructor(player, options) { constructor(player, options) {
super(player, options); super(player, options);
this.on(player, 'durationchange', this.updateContent); this.throttledUpdateContent = throttle(bind(this, this.updateContent), 25);
// Also listen for timeupdate and loadedmetadata because removing those this.on(player, [
// listeners could have broken dependent applications/libraries. These 'durationchange',
// can likely be removed for 6.0.
this.on(player, 'timeupdate', this.updateContent); // Also listen for timeupdate and loadedmetadata because removing those
this.on(player, 'loadedmetadata', this.updateContent); // listeners could have broken dependent applications/libraries. These
// can likely be removed for 7.0.
'loadedmetadata',
'timeupdate'
], this.throttledUpdateContent);
} }
/** /**
@ -45,18 +51,34 @@ class DurationDisplay extends Component {
}); });
this.contentEl_ = Dom.createEl('div', { this.contentEl_ = Dom.createEl('div', {
className: 'vjs-duration-display', className: 'vjs-duration-display'
// label the duration time for screen reader users
innerHTML: `<span class="vjs-control-text">${this.localize('Duration Time')}</span> 0:00`
}, { }, {
// tell screen readers not to automatically read the time as it changes // tell screen readers not to automatically read the time as it changes
'aria-live': 'off' 'aria-live': 'off'
}); }, Dom.createEl('span', {
className: 'vjs-control-text',
textContent: this.localize('Duration Time')
}));
this.updateTextNode_();
el.appendChild(this.contentEl_); el.appendChild(this.contentEl_);
return el; return el;
} }
/**
* Updates the "current time" text node with new content using the
* contents of the `formattedTime_` property.
*
* @private
*/
updateTextNode_() {
if (this.textNode_) {
this.contentEl_.removeChild(this.textNode_);
}
this.textNode_ = document.createTextNode(` ${this.formattedTime_ || '0:00'}`);
this.contentEl_.appendChild(this.textNode_);
}
/** /**
* Update duration time display. * Update duration time display.
* *
@ -73,14 +95,10 @@ class DurationDisplay extends Component {
if (duration && this.duration_ !== duration) { if (duration && this.duration_ !== duration) {
this.duration_ = duration; this.duration_ = duration;
const localizedText = this.localize('Duration Time'); this.formattedTime_ = formatTime(duration);
const formattedTime = formatTime(duration); this.requestAnimationFrame(this.updateTextNode_);
// label the duration time for screen reader users
this.contentEl_.innerHTML = `<span class="vjs-control-text">${localizedText}</span> ${formattedTime}`;
} }
} }
} }
Component.registerComponent('DurationDisplay', DurationDisplay); Component.registerComponent('DurationDisplay', DurationDisplay);

View File

@ -1,8 +1,10 @@
/** /**
* @file remaining-time-display.js * @file remaining-time-display.js
*/ */
import document from 'global/document';
import Component from '../../component.js'; import Component from '../../component.js';
import * as Dom from '../../utils/dom.js'; import * as Dom from '../../utils/dom.js';
import {bind, throttle} from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js'; import formatTime from '../../utils/format-time.js';
/** /**
@ -23,9 +25,8 @@ class RemainingTimeDisplay extends Component {
*/ */
constructor(player, options) { constructor(player, options) {
super(player, options); super(player, options);
this.throttledUpdateContent = throttle(bind(this, this.updateContent), 25);
this.on(player, 'timeupdate', this.updateContent); this.on(player, ['timeupdate', 'durationchange'], this.throttledUpdateContent);
this.on(player, 'durationchange', this.updateContent);
} }
/** /**
@ -40,18 +41,34 @@ class RemainingTimeDisplay extends Component {
}); });
this.contentEl_ = Dom.createEl('div', { this.contentEl_ = Dom.createEl('div', {
className: 'vjs-remaining-time-display', className: 'vjs-remaining-time-display'
// label the remaining time for screen reader users
innerHTML: `<span class="vjs-control-text">${this.localize('Remaining Time')}</span> -0:00`
}, { }, {
// tell screen readers not to automatically read the time as it changes // tell screen readers not to automatically read the time as it changes
'aria-live': 'off' 'aria-live': 'off'
}); }, Dom.createEl('span', {
className: 'vjs-control-text',
textContent: this.localize('Remaining Time')
}));
this.updateTextNode_();
el.appendChild(this.contentEl_); el.appendChild(this.contentEl_);
return el; return el;
} }
/**
* Updates the "remaining time" text node with new content using the
* contents of the `formattedTime_` property.
*
* @private
*/
updateTextNode_() {
if (this.textNode_) {
this.contentEl_.removeChild(this.textNode_);
}
this.textNode_ = document.createTextNode(` -${this.formattedTime_ || '0:00'}`);
this.contentEl_.appendChild(this.textNode_);
}
/** /**
* Update remaining time display. * Update remaining time display.
* *
@ -63,20 +80,14 @@ class RemainingTimeDisplay extends Component {
*/ */
updateContent(event) { updateContent(event) {
if (this.player_.duration()) { if (this.player_.duration()) {
const localizedText = this.localize('Remaining Time');
const formattedTime = formatTime(this.player_.remainingTime()); const formattedTime = formatTime(this.player_.remainingTime());
if (formattedTime !== this.formattedTime_) { if (formattedTime !== this.formattedTime_) {
this.formattedTime_ = formattedTime; this.formattedTime_ = formattedTime;
this.contentEl_.innerHTML = `<span class="vjs-control-text">${localizedText}</span> -${formattedTime}`; this.requestAnimationFrame(this.updateTextNode_);
} }
} }
// Allows for smooth scrubbing, when player can't keep up.
// var time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime();
// this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration());
} }
} }
Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);

View File

@ -368,24 +368,26 @@ class Html5 extends Tech {
// playback has started, which triggers the live display erroneously. // playback has started, which triggers the live display erroneously.
// Return NaN if playback has not started and trigger a durationupdate once // Return NaN if playback has not started and trigger a durationupdate once
// the duration can be reliably known. // the duration can be reliably known.
if (this.el_.duration === Infinity && if (
browser.IS_ANDROID && browser.IS_CHROME) { this.el_.duration === Infinity &&
if (this.el_.currentTime === 0) { browser.IS_ANDROID &&
// Wait for the first `timeupdate` with currentTime > 0 - there may be browser.IS_CHROME &&
// several with 0 this.el_.currentTime === 0
const checkProgress = () => { ) {
if (this.el_.currentTime > 0) { // Wait for the first `timeupdate` with currentTime > 0 - there may be
// Trigger durationchange for genuinely live video // several with 0
if (this.el_.duration === Infinity) { const checkProgress = () => {
this.trigger('durationchange'); if (this.el_.currentTime > 0) {
} // Trigger durationchange for genuinely live video
this.off('timeupdate', checkProgress); if (this.el_.duration === Infinity) {
this.trigger('durationchange');
} }
}; this.off('timeupdate', checkProgress);
}
};
this.on('timeupdate', checkProgress); this.on('timeupdate', checkProgress);
return NaN; return NaN;
}
} }
return this.el_.duration || NaN; return this.el_.duration || NaN;
} }

View File

@ -299,8 +299,9 @@ class TextTrackSettings extends ModalDialog {
* @param {string} key * @param {string} key
* Configuration key to use during creation. * Configuration key to use during creation.
* *
* @return {Element} * @return {string}
* The DOM element that gets created. * An HTML string.
*
* @private * @private
*/ */
createElSelect_(key, legendId = '', type = 'label') { createElSelect_(key, legendId = '', type = 'label') {
@ -308,101 +309,94 @@ class TextTrackSettings extends ModalDialog {
const id = config.id.replace('%s', this.id_); const id = config.id.replace('%s', this.id_);
return [ return [
createEl(type, { `<${type} id="${id}" class="${type === 'label' ? 'vjs-label' : ''}">`,
id, this.localize(config.label),
className: type === 'label' ? 'vjs-label' : '', `</${type}>`,
textContent: this.localize(config.label) `<select aria-labelledby="${legendId} ${id}">`
}, { ].
}), concat(config.options.map(o => {
createEl('select', {}, {
'aria-labelledby': `${legendId} ${id}`
}, config.options.map(o => {
const optionId = id + '-' + o[1]; const optionId = id + '-' + o[1];
return createEl('option', { return [
id: optionId, `<option id="${optionId}" value="${o[0]}" `,
textContent: this.localize(o[1]), `aria-labelledby="${legendId} ${id} ${optionId}">`,
value: o[0] this.localize(o[1]),
}, { '</option>'
'aria-labelledby': `${legendId} ${id} ${optionId}` ].join('');
}); })).
})) concat('</select>').join('');
];
} }
/** /**
* Create foreground color element for the component * Create foreground color element for the component
* *
* @return {Element} * @return {string}
* The element that was created. * An HTML string.
* *
* @private * @private
*/ */
createElFgColor_() { createElFgColor_() {
const legend = createEl('legend', { const legendId = `captions-text-legend-${this.id_}`;
id: `captions-text-legend-${this.id_}`,
textContent: this.localize('Text')
});
const select = this.createElSelect_('color', legend.id); return [
'<fieldset class="vjs-fg-color vjs-track-setting">',
const opacity = createEl('span', { `<legend id="${legendId}">`,
className: 'vjs-text-opacity vjs-opacity' this.localize('Text'),
}, undefined, this.createElSelect_('textOpacity', legend.id)); '</legend>',
this.createElSelect_('color', legendId),
return createEl('fieldset', { '<span class="vjs-text-opacity vjs-opacity">',
className: 'vjs-fg-color vjs-track-setting' this.createElSelect_('textOpacity', legendId),
}, undefined, [legend].concat(select, opacity)); '</span>',
'</fieldset>'
].join('');
} }
/** /**
* Create background color element for the component * Create background color element for the component
* *
* @return {Element} * @return {string}
* The element that was created * An HTML string.
* *
* @private * @private
*/ */
createElBgColor_() { createElBgColor_() {
const legend = createEl('legend', { const legendId = `captions-background-${this.id_}`;
id: `captions-background-${this.id_}`,
textContent: this.localize('Background')
});
const select = this.createElSelect_('backgroundColor', legend.id); return [
'<fieldset class="vjs-bg-color vjs-track-setting">',
const opacity = createEl('span', { `<legend id="${legendId}">`,
className: 'vjs-bg-opacity vjs-opacity' this.localize('Background'),
}, undefined, this.createElSelect_('backgroundOpacity', legend.id)); '</legend>',
this.createElSelect_('backgroundColor', legendId),
return createEl('fieldset', { '<span class="vjs-bg-opacity vjs-opacity">',
className: 'vjs-bg-color vjs-track-setting' this.createElSelect_('backgroundOpacity', legendId),
}, undefined, [legend].concat(select, opacity)); '</span>',
'</fieldset>'
].join('');
} }
/** /**
* Create window color element for the component * Create window color element for the component
* *
* @return {Element} * @return {string}
* The element that was created * An HTML string.
* *
* @private * @private
*/ */
createElWinColor_() { createElWinColor_() {
const legend = createEl('legend', { const legendId = `captions-window-${this.id_}`;
id: `captions-window-${this.id_}`,
textContent: this.localize('Window')
});
const select = this.createElSelect_('windowColor', legend.id); return [
'<fieldset class="vjs-window-color vjs-track-setting">',
const opacity = createEl('span', { `<legend id="${legendId}">`,
className: 'vjs-window-opacity vjs-opacity' this.localize('Window'),
}, undefined, this.createElSelect_('windowOpacity', legend.id)); '</legend>',
this.createElSelect_('windowColor', legendId),
return createEl('fieldset', { '<span class="vjs-window-opacity vjs-opacity">',
className: 'vjs-window-color vjs-track-setting' this.createElSelect_('windowOpacity', legendId),
}, undefined, [legend].concat(select, opacity)); '</span>',
'</fieldset>'
].join('');
} }
/** /**
@ -415,12 +409,13 @@ class TextTrackSettings extends ModalDialog {
*/ */
createElColors_() { createElColors_() {
return createEl('div', { return createEl('div', {
className: 'vjs-track-settings-colors' className: 'vjs-track-settings-colors',
}, undefined, [ innerHTML: [
this.createElFgColor_(), this.createElFgColor_(),
this.createElBgColor_(), this.createElBgColor_(),
this.createElWinColor_() this.createElWinColor_()
]); ].join('')
});
} }
/** /**
@ -432,21 +427,20 @@ class TextTrackSettings extends ModalDialog {
* @private * @private
*/ */
createElFont_() { createElFont_() {
const fontPercent = createEl('fieldset', {
className: 'vjs-font-percent vjs-track-setting'
}, undefined, this.createElSelect_('fontPercent', '', 'legend'));
const edgeStyle = createEl('fieldset', {
className: 'vjs-edge-style vjs-track-setting'
}, undefined, this.createElSelect_('edgeStyle', '', 'legend'));
const fontFamily = createEl('fieldset', {
className: 'vjs-font-family vjs-track-setting'
}, undefined, this.createElSelect_('fontFamily', '', 'legend'));
return createEl('div', { return createEl('div', {
className: 'vjs-track-settings-font' className: 'vjs-track-settings-font">',
}, undefined, [fontPercent, edgeStyle, fontFamily]); innerHTML: [
'<fieldset class="vjs-font-percent vjs-track-setting">',
this.createElSelect_('fontPercent', '', 'legend'),
'</fieldset>',
'<fieldset class="vjs-edge-style vjs-track-setting">',
this.createElSelect_('edgeStyle', '', 'legend'),
'</fieldset>',
'<fieldset class="vjs-font-family vjs-track-setting">',
this.createElSelect_('fontFamily', '', 'legend'),
'</fieldset>'
].join('')
});
} }
/** /**
@ -459,30 +453,17 @@ class TextTrackSettings extends ModalDialog {
*/ */
createElControls_() { createElControls_() {
const defaultsDescription = this.localize('restore all settings to the default values'); const defaultsDescription = this.localize('restore all settings to the default values');
const defaultsButton = createEl('button', {
className: 'vjs-default-button',
title: defaultsDescription,
innerHTML: `${this.localize('Reset')}<span class='vjs-control-text'> ${defaultsDescription}</span>`
});
const doneButton = createEl('button', {
className: 'vjs-done-button',
textContent: this.localize('Done')
});
return createEl('div', { return createEl('div', {
className: 'vjs-track-settings-controls' className: 'vjs-track-settings-controls',
}, undefined, [defaultsButton, doneButton]); innerHTML: [
} `<button class="vjs-default-button" title="${defaultsDescription}">`,
this.localize('Reset'),
/** `<span class="vjs-control-text"> ${defaultsDescription}</span>`,
* Create the component's DOM element '</button>',
* `<button class="vjs-done-button">${this.localize('Done')}</button>`
* @return {Element} ].join('')
* The element that was created. });
*/
createEl() {
return super.createEl();
} }
content() { content() {