jenkins-bot has submitted this change and it was merged.

Change subject: Optimize order of styles and scripts
......................................................................


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
---
M includes/EditPage.php
M includes/OutputPage.php
M includes/debug/MWDebug.php
M includes/resourceloader/ResourceLoader.php
M includes/resourceloader/ResourceLoaderStartUpModule.php
M includes/skins/Skin.php
M includes/specials/SpecialJavaScriptTest.php
M tests/phpunit/includes/OutputPageTest.php
8 files changed, 110 insertions(+), 84 deletions(-)

Approvals:
  Catrope: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/includes/EditPage.php b/includes/EditPage.php
index a8a17cf..5eb07d1 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -3693,7 +3693,7 @@
                }
 
                $script .= '});';
-               $wgOut->addScript( Html::inlineScript( 
ResourceLoader::makeLoaderConditionalScript( $script ) ) );
+               $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) 
);
 
                $toolbar = '<div id="toolbar"></div>';
 
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index edeae0d..5ad33fa 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -2674,10 +2674,12 @@
                        $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 @@
                                                        
$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 @@
                                        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 @@
                                                        // 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,11 +2951,39 @@
                }
 
                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;
@@ -2976,19 +3000,6 @@
                $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(
@@ -2999,17 +3010,6 @@
                        $this->getModuleScripts( true, 'top' ),
                        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 @@
                // 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 ) )
                        );
                }
 
diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php
index c4c6cf3..48d4e8d 100644
--- a/includes/debug/MWDebug.php
+++ b/includes/debug/MWDebug.php
@@ -419,10 +419,8 @@
 
                        // 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 ) )
                        );
                }
 
diff --git a/includes/resourceloader/ResourceLoader.php 
b/includes/resourceloader/ResourceLoader.php
index 5eab3cb..803afbf 100644
--- a/includes/resourceloader/ResourceLoader.php
+++ b/includes/resourceloader/ResourceLoader.php
@@ -1326,6 +1326,7 @@
         * 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
         */
@@ -1334,6 +1335,20 @@
        }
 
        /**
+        * 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.
         *
diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php 
b/includes/resourceloader/ResourceLoaderStartUpModule.php
index 48b3576..6ee098c 100644
--- a/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -357,11 +357,20 @@
                                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
diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php
index 48bce67..d649c2e 100644
--- a/includes/skins/Skin.php
+++ b/includes/skins/Skin.php
@@ -360,8 +360,8 @@
         */
        static function makeVariablesScript( $data ) {
                if ( $data ) {
-                       return Html::inlineScript(
-                               ResourceLoader::makeLoaderConditionalScript( 
ResourceLoader::makeConfigSetScript( $data ) )
+                       return ResourceLoader::makeInlineScript(
+                               ResourceLoader::makeConfigSetScript( $data )
                        );
                } else {
                        return '';
diff --git a/includes/specials/SpecialJavaScriptTest.php 
b/includes/specials/SpecialJavaScriptTest.php
index 492105d..7e3bd8f 100644
--- a/includes/specials/SpecialJavaScriptTest.php
+++ b/includes/specials/SpecialJavaScriptTest.php
@@ -168,15 +168,13 @@
                // 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 ) ) . '}'
+                               )
+                       ) )
                ) );
        }
 
diff --git a/tests/phpunit/includes/OutputPageTest.php 
b/tests/phpunit/includes/OutputPageTest.php
index 77a572d..1f35ead 100644
--- a/tests/phpunit/includes/OutputPageTest.php
+++ b/tests/phpunit/includes/OutputPageTest.php
@@ -141,52 +141,56 @@
                        // 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&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback&amp;*";></script>
+                               '<script 
src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;' .
+                               
'modules=test.raw&amp;only=scripts&amp;skin=fallback&amp;*"></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&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback&amp;*";>
+                               '<link rel=stylesheet 
href="http://127.0.0.1:8080/w/load.php?debug=false&amp;' .
+                               
'lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback&amp;*">
 '
                        ),
                        // 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&amp;lang=en&amp;modules=test.foo&amp;only=scripts&amp;skin=fallback&amp;*";
 /></script>
+                               '<script><esi:include 
src="http://127.0.0.1:8080/w/load.php?debug=false&amp;' .
+                               
'lang=en&amp;modules=test.foo&amp;only=scripts&amp;skin=fallback&amp;*" 
/></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&amp;lang=en&amp;modules=test.foo&amp;only=styles&amp;skin=fallback&amp;*";
 /></style>
+                               '<style><esi:include 
src="http://127.0.0.1:8080/w/load.php?debug=false&amp;' .
+                               
'lang=en&amp;modules=test.foo&amp;only=styles&amp;skin=fallback&amp;*" 
/></style>
 ',
                        ),
                        // Load no modules
@@ -197,18 +201,22 @@
                        // 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&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback&amp;*";></noscript>
+                               '<noscript><link rel=stylesheet 
href="http://127.0.0.1:8080/w/load.php?debug=' .
+                               
'false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback' 
.
+                               '&amp;*"></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>
 '
                        ),
                );

-- 
To view, visit https://gerrit.wikimedia.org/r/196989
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I98d383a6299ffbd10210431544a505338ca8643f
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Ori.livneh <[email protected]>
Gerrit-Reviewer: Aude <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Daniel Friesen <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: MZMcBride <[email protected]>
Gerrit-Reviewer: Ori.livneh <[email protected]>
Gerrit-Reviewer: PleaseStand <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to