1
0
mirror of https://github.com/videojs/video.js.git synced 2025-01-25 11:13:52 +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);
this.controlText_ = text;
this.controlTextEl_.innerHTML = localizedText;
Dom.textContent(this.controlTextEl_, localizedText);
if (!this.nonIconControl) {
// Set title attribute if only an icon is shown
el.setAttribute('title', localizedText);

View File

@ -1,8 +1,10 @@
/**
* @file current-time-display.js
*/
import document from 'global/document';
import Component from '../../component.js';
import * as Dom from '../../utils/dom.js';
import {bind, throttle} from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js';
/**
@ -23,8 +25,8 @@ class CurrentTimeDisplay extends Component {
*/
constructor(player, options) {
super(player, options);
this.on(player, 'timeupdate', this.updateContent);
this.throttledUpdateContent = throttle(bind(this, this.updateContent), 25);
this.on(player, 'timeupdate', this.throttledUpdateContent);
}
/**
@ -39,18 +41,34 @@ class CurrentTimeDisplay extends Component {
});
this.contentEl_ = Dom.createEl('div', {
className: 'vjs-current-time-display',
// label the current time for screen reader users
innerHTML: '<span class="vjs-control-text">Current Time </span>' + '0:00'
className: 'vjs-current-time-display'
}, {
// tell screen readers not to automatically read the time as it changes
'aria-live': 'off'
});
}, Dom.createEl('span', {
className: 'vjs-control-text',
textContent: this.localize('Current Time')
}));
this.updateTextNode_();
el.appendChild(this.contentEl_);
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
*
@ -62,12 +80,11 @@ class CurrentTimeDisplay extends Component {
updateContent(event) {
// Allows for smooth scrubbing, when player can't keep up.
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());
if (formattedTime !== this.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
*/
import document from 'global/document';
import Component from '../../component.js';
import * as Dom from '../../utils/dom.js';
import {bind, throttle} from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js';
/**
@ -24,13 +26,17 @@ class DurationDisplay extends Component {
constructor(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
// listeners could have broken dependent applications/libraries. These
// can likely be removed for 6.0.
this.on(player, 'timeupdate', this.updateContent);
this.on(player, 'loadedmetadata', this.updateContent);
this.on(player, [
'durationchange',
// Also listen for timeupdate and loadedmetadata because removing those
// 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', {
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`
className: 'vjs-duration-display'
}, {
// tell screen readers not to automatically read the time as it changes
'aria-live': 'off'
});
}, Dom.createEl('span', {
className: 'vjs-control-text',
textContent: this.localize('Duration Time')
}));
this.updateTextNode_();
el.appendChild(this.contentEl_);
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.
*
@ -73,14 +95,10 @@ class DurationDisplay extends Component {
if (duration && this.duration_ !== duration) {
this.duration_ = duration;
const localizedText = this.localize('Duration Time');
const formattedTime = formatTime(duration);
// label the duration time for screen reader users
this.contentEl_.innerHTML = `<span class="vjs-control-text">${localizedText}</span> ${formattedTime}`;
this.formattedTime_ = formatTime(duration);
this.requestAnimationFrame(this.updateTextNode_);
}
}
}
Component.registerComponent('DurationDisplay', DurationDisplay);

View File

@ -1,8 +1,10 @@
/**
* @file remaining-time-display.js
*/
import document from 'global/document';
import Component from '../../component.js';
import * as Dom from '../../utils/dom.js';
import {bind, throttle} from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js';
/**
@ -23,9 +25,8 @@ class RemainingTimeDisplay extends Component {
*/
constructor(player, options) {
super(player, options);
this.on(player, 'timeupdate', this.updateContent);
this.on(player, 'durationchange', this.updateContent);
this.throttledUpdateContent = throttle(bind(this, this.updateContent), 25);
this.on(player, ['timeupdate', 'durationchange'], this.throttledUpdateContent);
}
/**
@ -40,18 +41,34 @@ class RemainingTimeDisplay extends Component {
});
this.contentEl_ = Dom.createEl('div', {
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`
className: 'vjs-remaining-time-display'
}, {
// tell screen readers not to automatically read the time as it changes
'aria-live': 'off'
});
}, Dom.createEl('span', {
className: 'vjs-control-text',
textContent: this.localize('Remaining Time')
}));
this.updateTextNode_();
el.appendChild(this.contentEl_);
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.
*
@ -63,20 +80,14 @@ class RemainingTimeDisplay extends Component {
*/
updateContent(event) {
if (this.player_.duration()) {
const localizedText = this.localize('Remaining Time');
const formattedTime = formatTime(this.player_.remainingTime());
if (formattedTime !== this.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);

View File

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

View File

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