Optimize order of styles and scripts
The current ordering of scripts and stylesheets in <head> causes all major browsers to serialize and defer requests that could be performed in parallel. The problem is that external stylesheets are loaded before inline scripts. As Steven Souders explains, "all major browsers preserve the order of CSS and JavaScript. The stylesheet has to be fully downloaded, parsed, and applied before the inline script is executed. And the inline script must be executed before the remaining resources can be downloaded. Therefore, resources that follow a stylesheet and inline script are blocked from downloading."[1] In other words: the browser could start loading body images, but it refuses to do that until it has executed inline scripts in head. And it refuses to execute those scripts until the external CSS is downloaded, parsed and applied. You can see the effect of this in this image, showing the request waterfall for [[en:Gothic Alphabet]]: [2]. Notice how no images were requested before the browser had finished processing the three load.php requests at the top. To fix this, we want to move the inline scripts above the external CSS. This is a little bit tricky, because the inline scripts depend on mw.loader, which is loaded via an external script. If we move the external script so that it too is above the external stylesheet, we force the browser to serialize requests, because the browser will not retrieve the external CSS until it has retrieved and executed the external JS code. So what we want is to move the inline scripts above the external stylesheet, but keep the external script (which the inline scripts depend on) below the external stylesheet. We can do this by wrapping the inline script code in a closure (which binds 'mw') and enqueuing the closure in a global array which will be processed by the startup module at just the right time. Net result: external CSS and JS is retrieved in parallel, retrieval of images (and other external assets) is unblocked, but the order in which code is evaluated remains the same. [1]: <http://www.stevesouders.com/blog/2009/05/06/positioning-inline-scripts/> [2]: <http://people.wikimedia.org/~ori/enwiki-waterfall.png> (excerpted from <http://www.webpagetest.org/result/150316_0C_7MB/1/details/>. Change-Id: I98d383a6299ffbd10210431544a505338ca8643f
This commit is contained in:
parent
5ae7b2159c
commit
e86e5f8460
8 changed files with 110 additions and 84 deletions
|
|
@ -3693,7 +3693,7 @@ HTML
|
|||
}
|
||||
|
||||
$script .= '});';
|
||||
$wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) );
|
||||
$wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
|
||||
|
||||
$toolbar = '<div id="toolbar"></div>';
|
||||
|
||||
|
|
|
|||
|
|
@ -2674,10 +2674,12 @@ class OutputPage extends ContextSource {
|
|||
$ret .= $item . "\n";
|
||||
}
|
||||
|
||||
$ret .= $this->getInlineHeadScript();
|
||||
|
||||
// No newline after buildCssLinks since makeResourceLoaderLink did that already
|
||||
$ret .= $this->buildCssLinks();
|
||||
|
||||
$ret .= $this->getHeadScripts() . "\n";
|
||||
$ret .= $this->getHeadScripts();
|
||||
|
||||
foreach ( $this->mHeadItems as $item ) {
|
||||
$ret .= $item . "\n";
|
||||
|
|
@ -2854,10 +2856,8 @@ class OutputPage extends ContextSource {
|
|||
$resourceLoader->makeModuleResponse( $context, $grpModules )
|
||||
);
|
||||
} else {
|
||||
$links['html'] .= Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
$resourceLoader->makeModuleResponse( $context, $grpModules )
|
||||
)
|
||||
$links['html'] .= ResourceLoader::makeInlineScript(
|
||||
$resourceLoader->makeModuleResponse( $context, $grpModules )
|
||||
);
|
||||
}
|
||||
$links['html'] .= "\n";
|
||||
|
|
@ -2896,10 +2896,8 @@ class OutputPage extends ContextSource {
|
|||
if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
|
||||
$link = Html::linkedStyle( $url );
|
||||
} elseif ( $loadCall ) {
|
||||
$link = Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
|
||||
)
|
||||
$link = ResourceLoader::makeInlineScript(
|
||||
Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
|
||||
);
|
||||
} else {
|
||||
$link = Html::linkedScript( $url );
|
||||
|
|
@ -2908,10 +2906,8 @@ class OutputPage extends ContextSource {
|
|||
// browsers not supported by the startup module would unconditionally
|
||||
// execute this module. Otherwise users will get "ReferenceError: mw is
|
||||
// undefined" or "jQuery is undefined" from e.g. a "site" module.
|
||||
$link = Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
Xml::encodeJsCall( 'document.write', array( $link ) )
|
||||
)
|
||||
$link = ResourceLoader::makeInlineScript(
|
||||
Xml::encodeJsCall( 'document.write', array( $link ) )
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2955,16 +2951,44 @@ class OutputPage extends ContextSource {
|
|||
}
|
||||
|
||||
if ( count( $states ) ) {
|
||||
$html = Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
ResourceLoader::makeLoaderStateScript( $states )
|
||||
)
|
||||
$html = ResourceLoader::makeInlineScript(
|
||||
ResourceLoader::makeLoaderStateScript( $states )
|
||||
) . "\n" . $html;
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get <script> tags for <head> whose source is inline.
|
||||
*
|
||||
* @since 1.25
|
||||
* @return string HTML fragment
|
||||
*/
|
||||
public function getInlineHeadScript() {
|
||||
// Load config before anything else.
|
||||
$html = ResourceLoader::makeInlineScript(
|
||||
ResourceLoader::makeConfigSetScript( $this->getJSVars() )
|
||||
);
|
||||
|
||||
// Load embeddable private modules before any loader links.
|
||||
$inlineModulesLink = $this->makeResourceLoaderLink(
|
||||
array( 'user.options', 'user.tokens' ), ResourceLoaderModule::TYPE_COMBINED
|
||||
);
|
||||
$html .= "\n" . self::getHtmlFromLoaderLinks( array( $inlineModulesLink ) );
|
||||
|
||||
// Construct mw.loader.load() call for top-loaded modules.
|
||||
// Client-side code will request these modules and their dependencies.
|
||||
$topModules = $this->getModules( true, 'top' );
|
||||
if ( $topModules ) {
|
||||
$html .= ResourceLoader::makeInlineScript(
|
||||
Xml::encodeJsCall( 'mw.loader.load', array( $topModules ) )
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* JS stuff to put in the "<head>". This is the startup module, config
|
||||
* vars and modules marked with position 'top'
|
||||
|
|
@ -2976,19 +3000,6 @@ class OutputPage extends ContextSource {
|
|||
$links = array();
|
||||
$links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
|
||||
|
||||
// Load config before anything else
|
||||
$links[] = Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
ResourceLoader::makeConfigSetScript( $this->getJSVars() )
|
||||
)
|
||||
);
|
||||
|
||||
// Load embeddable private modules before any loader links
|
||||
// This needs to be TYPE_COMBINED so these modules are properly wrapped
|
||||
// in mw.loader.implement() calls and deferred until mw.user is available
|
||||
$embedScripts = array( 'user.options', 'user.tokens' );
|
||||
$links[] = $this->makeResourceLoaderLink( $embedScripts, ResourceLoaderModule::TYPE_COMBINED );
|
||||
|
||||
// Scripts and messages "only" requests marked for top inclusion
|
||||
// Messages should go first
|
||||
$links[] = $this->makeResourceLoaderLink(
|
||||
|
|
@ -3000,17 +3011,6 @@ class OutputPage extends ContextSource {
|
|||
ResourceLoaderModule::TYPE_SCRIPTS
|
||||
);
|
||||
|
||||
// Modules requests - let the client calculate dependencies and batch requests as it likes
|
||||
// Only load modules that have marked themselves for loading at the top
|
||||
$modules = $this->getModules( true, 'top' );
|
||||
if ( $modules ) {
|
||||
$links[] = Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
Xml::encodeJsCall( 'mw.loader.load', array( $modules ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) {
|
||||
$links[] = $this->getScriptsForBottomQueue( true );
|
||||
}
|
||||
|
|
@ -3047,10 +3047,8 @@ class OutputPage extends ContextSource {
|
|||
// Only load modules that have marked themselves for loading at the bottom
|
||||
$modules = $this->getModules( true, 'bottom' );
|
||||
if ( $modules ) {
|
||||
$links[] = Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) )
|
||||
)
|
||||
$links[] = ResourceLoader::makeInlineScript(
|
||||
Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) )
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -419,10 +419,8 @@ class MWDebug {
|
|||
|
||||
// Cannot use OutputPage::addJsConfigVars because those are already outputted
|
||||
// by the time this method is called.
|
||||
$html = Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) )
|
||||
)
|
||||
$html = ResourceLoader::makeInlineScript(
|
||||
ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) )
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1326,6 +1326,7 @@ MESSAGE;
|
|||
* Returns JS code which runs given JS code if the client-side framework is
|
||||
* present.
|
||||
*
|
||||
* @deprecated since 1.25; use makeInlineScript instead
|
||||
* @param string $script JavaScript code
|
||||
* @return string
|
||||
*/
|
||||
|
|
@ -1333,6 +1334,20 @@ MESSAGE;
|
|||
return "if(window.mw){\n" . trim( $script ) . "\n}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an inline script tag with given JS code.
|
||||
*
|
||||
* The code will be wrapped in a closure, and it will be executed by ResourceLoader
|
||||
* only if the client has adequate support for MediaWiki JavaScript code.
|
||||
*
|
||||
* @param string $script JavaScript code
|
||||
* @return string HTML
|
||||
*/
|
||||
public static function makeInlineScript( $script ) {
|
||||
$js = 'var _mwq = _mwq || []; _mwq.push( function ( mw ) { ' . $script . ' } );';
|
||||
return Html::inlineScript( $js );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns JS code which will set the MediaWiki configuration array to
|
||||
* the given value.
|
||||
|
|
|
|||
|
|
@ -357,11 +357,20 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
|
|||
ResourceLoader::inDebugMode()
|
||||
);
|
||||
|
||||
// Process the deferred inline script queue, and ensure that any
|
||||
// functions enqueued after this point are executed immediately.
|
||||
$mwqJs = (
|
||||
'window._mwq = window._mwq || [];' .
|
||||
'while ( _mwq.length ) _mwq.shift()( mw );' .
|
||||
'_mwq.push = function ( f ) { f( mw ); };'
|
||||
);
|
||||
|
||||
$out .= "var startUp = function () {\n" .
|
||||
"\tmw.config = new " .
|
||||
$mwMapJsCall . "\n" .
|
||||
"\t$registrations\n" .
|
||||
"\t" . $mwConfigSetJsCall .
|
||||
"\t" . $mwConfigSetJsCall . "\n" .
|
||||
"\t" . $mwqJs . "\n" .
|
||||
"};\n";
|
||||
|
||||
// Conditional script injection
|
||||
|
|
|
|||
|
|
@ -360,8 +360,8 @@ abstract class Skin extends ContextSource {
|
|||
*/
|
||||
static function makeVariablesScript( $data ) {
|
||||
if ( $data ) {
|
||||
return Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript( ResourceLoader::makeConfigSetScript( $data ) )
|
||||
return ResourceLoader::makeInlineScript(
|
||||
ResourceLoader::makeConfigSetScript( $data )
|
||||
);
|
||||
} else {
|
||||
return '';
|
||||
|
|
|
|||
|
|
@ -168,15 +168,13 @@ HTML;
|
|||
// The testrunner configures QUnit and essentially depends on it. However, test suites
|
||||
// are reusable in environments that preload QUnit (or a compatibility interface to
|
||||
// another framework). Therefore we have to load it ourselves.
|
||||
$out->addHtml( Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
Xml::encodeJsCall( 'mw.loader.using', array(
|
||||
array( 'jquery.qunit', 'jquery.qunit.completenessTest' ),
|
||||
new XmlJsCode(
|
||||
'function () {' . Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) . '}'
|
||||
)
|
||||
) )
|
||||
)
|
||||
$out->addHtml( ResourceLoader::makeInlineScript(
|
||||
Xml::encodeJsCall( 'mw.loader.using', array(
|
||||
array( 'jquery.qunit', 'jquery.qunit.completenessTest' ),
|
||||
new XmlJsCode(
|
||||
'function () {' . Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) . '}'
|
||||
)
|
||||
) )
|
||||
) );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,52 +141,56 @@ class OutputPageTest extends MediaWikiTestCase {
|
|||
// Load module script only
|
||||
array(
|
||||
array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ),
|
||||
'<script>if(window.mw){
|
||||
document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.foo\u0026amp;only=scripts\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");
|
||||
}</script>
|
||||
'<script>var _mwq = _mwq || []; _mwq.push( function ( mw ) {' .
|
||||
' document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=' .
|
||||
'false\u0026amp;lang=en\u0026amp;modules=test.foo\u0026amp;only=scripts\u0026' .
|
||||
'amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E"); } );</script>
|
||||
'
|
||||
),
|
||||
array(
|
||||
// Don't condition wrap raw modules (like the startup module)
|
||||
array( 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ),
|
||||
'<script src="http://127.0.0.1:8080/w/load.php?debug=false&lang=en&modules=test.raw&only=scripts&skin=fallback&*"></script>
|
||||
'<script src="http://127.0.0.1:8080/w/load.php?debug=false&lang=en&' .
|
||||
'modules=test.raw&only=scripts&skin=fallback&*"></script>
|
||||
'
|
||||
),
|
||||
// Load module styles only
|
||||
// This also tests the order the modules are put into the url
|
||||
array(
|
||||
array( array( 'test.baz', 'test.foo', 'test.bar' ), ResourceLoaderModule::TYPE_STYLES ),
|
||||
'<link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&lang=en&modules=test.bar%2Cbaz%2Cfoo&only=styles&skin=fallback&*">
|
||||
'<link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&' .
|
||||
'lang=en&modules=test.bar%2Cbaz%2Cfoo&only=styles&skin=fallback&*">
|
||||
'
|
||||
),
|
||||
// Load private module (only=scripts)
|
||||
array(
|
||||
array( 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ),
|
||||
'<script>if(window.mw){
|
||||
mw.test.baz({token:123});mw.loader.state({"test.quux":"ready"});
|
||||
|
||||
}</script>
|
||||
'<script>var _mwq = _mwq || []; _mwq.push( function ( mw ) {' .
|
||||
' mw.test.baz({token:123});mw.loader.state({"test.quux":"ready"});
|
||||
' . ' } );</script>
|
||||
'
|
||||
),
|
||||
// Load private module (combined)
|
||||
array(
|
||||
array( 'test.quux', ResourceLoaderModule::TYPE_COMBINED ),
|
||||
'<script>if(window.mw){
|
||||
mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"css":[".mw-icon{transition:none}\n"]},{},{});
|
||||
|
||||
}</script>
|
||||
'<script>var _mwq = _mwq || []; _mwq.push( function ( mw ) {' .
|
||||
' mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});}' .
|
||||
',{"css":[".mw-icon{transition:none}\n"]},{},{});
|
||||
' . ' } );</script>
|
||||
'
|
||||
),
|
||||
// Load module script with ESI
|
||||
array(
|
||||
array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS, true ),
|
||||
'<script><esi:include src="http://127.0.0.1:8080/w/load.php?debug=false&lang=en&modules=test.foo&only=scripts&skin=fallback&*" /></script>
|
||||
'<script><esi:include src="http://127.0.0.1:8080/w/load.php?debug=false&' .
|
||||
'lang=en&modules=test.foo&only=scripts&skin=fallback&*" /></script>
|
||||
'
|
||||
),
|
||||
// Load module styles with ESI
|
||||
array(
|
||||
array( 'test.foo', ResourceLoaderModule::TYPE_STYLES, true ),
|
||||
'<style><esi:include src="http://127.0.0.1:8080/w/load.php?debug=false&lang=en&modules=test.foo&only=styles&skin=fallback&*" /></style>
|
||||
'<style><esi:include src="http://127.0.0.1:8080/w/load.php?debug=false&' .
|
||||
'lang=en&modules=test.foo&only=styles&skin=fallback&*" /></style>
|
||||
',
|
||||
),
|
||||
// Load no modules
|
||||
|
|
@ -197,18 +201,22 @@ mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"
|
|||
// noscript group
|
||||
array(
|
||||
array( 'test.noscript', ResourceLoaderModule::TYPE_STYLES ),
|
||||
'<noscript><link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&lang=en&modules=test.noscript&only=styles&skin=fallback&*"></noscript>
|
||||
'<noscript><link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=' .
|
||||
'false&lang=en&modules=test.noscript&only=styles&skin=fallback' .
|
||||
'&*"></noscript>
|
||||
'
|
||||
),
|
||||
// Load two modules in separate groups
|
||||
array(
|
||||
array( array( 'test.group.foo', 'test.group.bar' ), ResourceLoaderModule::TYPE_COMBINED ),
|
||||
'<script>if(window.mw){
|
||||
document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.group.bar\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");
|
||||
}</script>
|
||||
<script>if(window.mw){
|
||||
document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.group.foo\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");
|
||||
}</script>
|
||||
'<script>var _mwq = _mwq || []; _mwq.push( function ( mw ) { ' .
|
||||
'document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false' .
|
||||
'\u0026amp;lang=en\u0026amp;modules=test.group.bar\u0026amp;skin=fallback\u0026' .
|
||||
'amp;*\"\u003E\u003C/script\u003E"); } );</script>
|
||||
<script>var _mwq = _mwq || []; _mwq.push( function ( mw ) { document.write(' .
|
||||
'"\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;' .
|
||||
'lang=en\u0026amp;modules=test.group.foo\u0026amp;skin=fallback\u0026amp;*\\' .
|
||||
'"\u003E\u003C/script\u003E"); } );</script>
|
||||
'
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue