compactlinks: Optimise performance of DOM logic

The createCompactList() function runs synchronously during the
module execution burst. Due to it visually changing the page, I
won't defer it with rIC for the time being, although that should
be considered for the future. For this commit, I'm trying to make
it fit the budget of <50ms because ULS is currently usually taking
80ms-180ms on desktop (MacBook/Chrome CPU/4), and that's during
batch execution with other modules as well, thus freezing the
UI thread for much longer than that.

constructor:
* Remove needless clone of jQuery object.
  Use $foo instead of $( $foo ).
* Remove creation of 'interlanguageList' and 'compactList' objects
  that are immediately removed and re-created by init().

init/getInterlanguageList:
* Use the HTMLElement.lang and HTMLAnchorElement.href properties
  directly instead of the DOM getAttribute().
  This means stores a full url instead of a relative url, which
  should help avoid other bugs in the future.
* Remove needless jQuery() constructor and jQuery.text() call.
  Use Node.textContent directly instead.
* Use HTMLElement#querySelectorAll instead of jQuery#find().

init/getCompactList/../filterByLangsInText:
* Avoid jQuery() constructor and jQuery.attr(),
  use the HTMLElement.lang property directly.
* Avoid jQuery() selector, call querySelectorAll directly.

init/getCompactList/../getCommonLanguages/../getFrequentLanguageList:
* Avoid temporary array copies from concat() and function overhead
  with forEach() and filter().
  Instead, keep only a single array, and iterate it once.

init/getCompactList/../filterByBadges (~10m -> ~0.5ms):
* Use one query via $(), instead of two queries $()+find().
* Use $.map() directly instead of map()+fakejQueryObject+toArray().
* Use querySelector(One) for the child instead of $()+find().
* Use HTMLElement.lang property directly.

init/hideOriginal (~5m -> ~0.8ms):
* Use querySelectorAll() directly instead of jQuery find().
* Set HTMLElement.style directly instead of jQuery() css().

init/render/addTrigger:
* Use createElement() and direct properties instead of $(), addClass(),
  prop() and text().
* The mw.msg() calls use text() and jqueryMsg#parser which is
  expensive.
  Use plain() for 'ext-uls-compact-link-info', which doesn't need parsing.
  Keep text() for the other message, and document why.

init/listen:
* Use async Deferred#then() instead of sometimes-sync Deferred#done().

Bug: T127328
Change-Id: I424c34fb82c8e95407f7b934e6d42019becbf909
This commit is contained in:
Timo Tijhof
2018-09-07 05:55:56 +01:00
committed by jenkins-bot
parent 00b1ea40ae
commit effcd80471
2 changed files with 101 additions and 68 deletions

View File

@@ -180,35 +180,34 @@
* @return {Array} List of language codes without duplicates. * @return {Array} List of language codes without duplicates.
*/ */
mw.uls.getFrequentLanguageList = function ( countryCode ) { mw.uls.getFrequentLanguageList = function ( countryCode ) {
var unique = [], var i, j, lang,
list = [ ret = [],
lists = [
[
mw.config.get( 'wgUserLanguage' ), mw.config.get( 'wgUserLanguage' ),
mw.config.get( 'wgContentLanguage' ), mw.config.get( 'wgContentLanguage' ),
mw.uls.getBrowserLanguage() mw.uls.getBrowserLanguage()
] ],
.concat( mw.uls.getPreviousLanguages() ) mw.uls.getPreviousLanguages(),
.concat( mw.uls.getAcceptLanguageList() ); mw.uls.getAcceptLanguageList()
];
countryCode = countryCode || mw.uls.getCountryCode(); countryCode = countryCode || mw.uls.getCountryCode();
if ( countryCode ) { if ( countryCode ) {
list = list.concat( $.uls.data.getLanguagesInTerritory( countryCode ) ); lists.push( $.uls.data.getLanguagesInTerritory( countryCode ) );
} }
list.forEach( function ( lang ) { for ( i = 0; i < lists.length; i++ ) {
if ( unique.indexOf( lang ) === -1 ) { for ( j = 0; j < lists[ i ].length; j++ ) {
unique.push( lang ); lang = lists[ i ][ j ];
// Make flat, make unique, and ignore unknown/unsupported languages
if ( ret.indexOf( lang ) === -1 && $.uls.data.getAutonym( lang ) !== lang ) {
ret.push( lang );
}
}
} }
} );
// Filter out unknown and unsupported languages return ret;
unique = unique.filter( function ( langCode ) {
// If the language is already known and defined, just use it.
// $.uls.data.getAutonym will resolve redirects if any.
return $.uls.data.getAutonym( langCode ) !== langCode;
} );
return unique;
}; };
}() ); }() );

View File

@@ -72,7 +72,7 @@
* @return {string[]} List of language codes supported by the article * @return {string[]} List of language codes supported by the article
*/ */
function filterByBabelLanguages( languages ) { function filterByBabelLanguages( languages ) {
var babelLanguages = mw.config.get( 'wgULSBabelLanguages', [] ); var babelLanguages = mw.config.get( 'wgULSBabelLanguages' ) || [];
return babelLanguages.filter( function ( language ) { return babelLanguages.filter( function ( language ) {
return languages.indexOf( language ) >= 0; return languages.indexOf( language ) >= 0;
@@ -86,8 +86,7 @@
* @return {string[]} List of language codes supported by the article * @return {string[]} List of language codes supported by the article
*/ */
function filterBySitePicks( languages ) { function filterBySitePicks( languages ) {
var picks = mw.config.get( 'wgULSCompactLinksPrepend', [] ); var picks = mw.config.get( 'wgULSCompactLinksPrepend' ) || [];
return picks.filter( function ( language ) { return picks.filter( function ( language ) {
return languages.indexOf( language ) >= 0; return languages.indexOf( language ) >= 0;
} ); } );
@@ -136,27 +135,37 @@
*/ */
function filterByAssistantLanguages( languages ) { function filterByAssistantLanguages( languages ) {
var assistantLanguages = mw.user.options.get( 'translate-editlangs' ); var assistantLanguages = mw.user.options.get( 'translate-editlangs' );
if ( !assistantLanguages || assistantLanguages === 'default' ) {
return [];
}
if ( assistantLanguages && assistantLanguages !== 'default' ) {
return assistantLanguages.split( /,\s*/ ).filter( function ( language ) { return assistantLanguages.split( /,\s*/ ).filter( function ( language ) {
return languages.indexOf( language ) >= 0; return languages.indexOf( language ) >= 0;
} ); } );
} }
return [];
}
/** /**
* @class * @class
* @constructor * @constructor
* @param {string|jQuery} interlanguageList Selector for interlanguage list * @param {HTMLElement} listElement Interlanguage list element
* @param {Object} options * @param {Object} options
*/ */
function CompactInterlanguageList( interlanguageList, options ) { function CompactInterlanguageList( listElement, options ) {
this.$interlanguageList = $( interlanguageList ); this.listElement = listElement;
this.options = options || {}; this.options = options || {};
this.interlanguageList = {};
this.compactList = {}; /**
* @private
* @property {Object} interlanguageList
*/
this.interlanguageList = null;
/**
* @private
* @property {Object} interlanguageList
*/
this.compactList = null;
this.commonInterlanguageList = null; this.commonInterlanguageList = null;
this.$trigger = null; this.$trigger = null;
this.compactSize = 0; this.compactSize = 0;
@@ -176,7 +185,6 @@
if ( this.listSize <= max ) { if ( this.listSize <= max ) {
// Not enough languages to compact the list // Not enough languages to compact the list
mw.hook( 'mw.uls.compactlinks.initialized' ).fire( false ); mw.hook( 'mw.uls.compactlinks.initialized' ).fire( false );
return; return;
} }
@@ -311,7 +319,7 @@
this.$trigger.one( 'click', function () { this.$trigger.one( 'click', function () {
// Load the ULS now. // Load the ULS now.
mw.loader.using( 'ext.uls.mediawiki' ).done( function () { mw.loader.using( 'ext.uls.mediawiki' ).then( function () {
self.createSelector( self.$trigger ); self.createSelector( self.$trigger );
self.$trigger.click(); self.$trigger.click();
} ); } );
@@ -324,11 +332,10 @@
* @return {Object} * @return {Object}
*/ */
CompactInterlanguageList.prototype.getCompactList = function () { CompactInterlanguageList.prototype.getCompactList = function () {
var language, languages, compactLanguages, i, var language, languages, compactLanguages, i, compactedList;
compactedList = {}; compactedList = {};
languages = Object.keys( this.interlanguageList ); languages = Object.keys( this.interlanguageList );
compactLanguages = this.compact( languages ); compactLanguages = this.compact( languages );
for ( i = 0; i < compactLanguages.length; i++ ) { for ( i = 0; i < compactLanguages.length; i++ ) {
@@ -413,9 +420,8 @@
*/ */
CompactInterlanguageList.prototype.filterByLangsInText = function ( languages ) { CompactInterlanguageList.prototype.filterByLangsInText = function ( languages ) {
var languagesInText = []; var languagesInText = [];
$.each( document.querySelectorAll( '#mw-content-text [lang]' ), function ( i, el ) {
$( '#mw-content-text [lang]' ).each( function ( i, el ) { var lang = convertMediaWikiLanguageCodeToULS( el.lang );
var lang = convertMediaWikiLanguageCodeToULS( $( el ).attr( 'lang' ) );
if ( languagesInText.indexOf( lang ) === -1 && languages.indexOf( lang ) >= 0 ) { if ( languagesInText.indexOf( lang ) === -1 && languages.indexOf( lang ) >= 0 ) {
languagesInText.push( lang ); languagesInText.push( lang );
} }
@@ -435,11 +441,14 @@
* @return {Array} List of language codes in which there are articles with badges * @return {Array} List of language codes in which there are articles with badges
*/ */
CompactInterlanguageList.prototype.filterByBadges = function () { CompactInterlanguageList.prototype.filterByBadges = function () {
return $( '#p-lang' ).find( '[class*="badge"]' ).map( function ( i, el ) { return $.map(
document.querySelectorAll( '#p-lang [class*="badge"]' ),
function ( el ) {
return convertMediaWikiLanguageCodeToULS( return convertMediaWikiLanguageCodeToULS(
$( el ).find( '.interlanguage-link-target' ).attr( 'lang' ) el.querySelector( '.interlanguage-link-target' ).lang
);
}
); );
} ).toArray();
}; };
/** /**
@@ -451,13 +460,12 @@
CompactInterlanguageList.prototype.getInterlanguageList = function () { CompactInterlanguageList.prototype.getInterlanguageList = function () {
var interlanguageList = {}; var interlanguageList = {};
this.$interlanguageList.find( '.interlanguage-link-target' ).each( function () { $.each( this.listElement.querySelectorAll( '.interlanguage-link-target' ), function ( i, el ) {
var langCode = convertMediaWikiLanguageCodeToULS( this.getAttribute( 'lang' ) ); var langCode = convertMediaWikiLanguageCodeToULS( el.lang );
interlanguageList[ langCode ] = { interlanguageList[ langCode ] = {
href: this.getAttribute( 'href' ), href: el.href,
autonym: $( this ).text(), autonym: el.textContent,
element: this element: el
}; };
} ); } );
@@ -486,29 +494,55 @@
* Hide the original interlanguage list * Hide the original interlanguage list
*/ */
CompactInterlanguageList.prototype.hideOriginal = function () { CompactInterlanguageList.prototype.hideOriginal = function () {
this.$interlanguageList.find( '.interlanguage-link' ).css( 'display', 'none' ); var links = this.listElement.querySelectorAll( '.interlanguage-link' ),
i = links.length;
while ( i-- ) {
links[ i ].style.display = 'none';
}
}; };
/** /**
* Add the trigger at the bottom of the language list * Add the trigger at the bottom of the language list
*/ */
CompactInterlanguageList.prototype.addTrigger = function () { CompactInterlanguageList.prototype.addTrigger = function () {
var $trigger; var trigger = document.createElement( 'button' );
trigger.className = 'mw-interlanguage-selector mw-ui-button';
$trigger = $( '<button>' ) trigger.title = mw.message( 'ext-uls-compact-link-info' ).plain();
.addClass( 'mw-interlanguage-selector mw-ui-button' ) // Use text() because the message needs {{PLURAL:}}
.prop( 'title', mw.msg( 'ext-uls-compact-link-info' ) ) trigger.textContent = mw.message(
.text( mw.msg(
'ext-uls-compact-link-count', 'ext-uls-compact-link-count',
mw.language.convertNumber( this.listSize - this.compactSize ) mw.language.convertNumber( this.listSize - this.compactSize )
) ); ).text();
this.$interlanguageList.append( $trigger ); this.listElement.appendChild( trigger );
this.$trigger = $trigger; this.$trigger = $( trigger );
}; };
/**
* Performance cost of calling createCompactList(), as of 2018-09-10.
*
* Summary:
* - DOM Queries: 5 + 1N
* * createCompactList (1 querySelector)
* * filterByBadges (1N querySelector, 1 querySelectorAll)
* * getInterlanguageList (1 querySelectorAll)
* * filterByLangsInText (1 querySelectorAll)
* * hideOriginal (1 querySelectorAll)
* - DOM Writes: 1 + 2N
* * addTrigger (1 appendChild)
* * hideOriginal (1N Element.style)
* * render (1N Element.style)
* - Misc: 1
* * addTrigger (1 mw.Message#parser)
*/
function createCompactList() { function createCompactList() {
var compactList = new CompactInterlanguageList( $( '#p-lang ul' ), { var listElement, compactList;
listElement = document.querySelector( '#p-lang ul' );
if ( !listElement ) {
// Not all namespaces/pages/actions have #p-lang.
return;
}
compactList = new CompactInterlanguageList( listElement, {
// Compact the list to this size // Compact the list to this size
max: 9 max: 9
} ); } );