ResourceLoader: Add an experimental option to move the main module loading queue...
authorRoan Kattouw <catrope@users.mediawiki.org>
Thu, 5 Jan 2012 23:32:41 +0000 (23:32 +0000)
committerRoan Kattouw <catrope@users.mediawiki.org>
Thu, 5 Jan 2012 23:32:41 +0000 (23:32 +0000)
* Added a "blocking" state to mw.loader . When loading scripts while the document is not ready, the loader will use document.write() if blocking is true, and append to the <body> or the <head> if blocking is false. If the document is ready, the loader will always append to the <body>
* Enable blocking mode while loading the top queue, and disable it after. This ensures that modules in the top queue are still loaded in a blocking way as they were before
* If $wgResourceLoaderExperimentalAsyncLoading is true, the bottom queue is also loaded in the head, but with blocking mode disabled. Otherwise, it's loaded at the bottom of the <body> as before
* scripts-only and messages-only requests need special treatment:
** in the top queue, they can continue to use <script src="..."> tags because they are blocking
** if the bottom queue is at the bottom of the <body> (experimental async loading disabled), they can continue to use <script src="..."> tags as before
** if the bottom queue is in the <head> (experimental async loading enabled), they cannot use <script src="..."> tags, because those would block. Instead, call mw.loader.load() on the load.php URL

includes/DefaultSettings.php
includes/OutputPage.php
resources/mediawiki/mediawiki.js

index 55baab8..a9a203b 100644 (file)
@@ -2646,6 +2646,13 @@ $wgResourceLoaderValidateJS = true;
  */
 $wgResourceLoaderValidateStaticJS = false;
 
+/**
+ * If set to true, asynchronous loading of bottom-queue scripts in the <head>
+ * will be enabled. This is an experimental feature that's supposed to make
+ * JavaScript load faster.
+ */
+$wgResourceLoaderExperimentalAsyncLoading = false;
+
 /** @} */ # End of resource loader settings }
 
 
index 090a08b..ee1ee4f 100644 (file)
@@ -2494,9 +2494,10 @@ $templates
         * @param $only String ResourceLoaderModule TYPE_ class constant
         * @param $useESI boolean
         * @param $extraQuery Array with extra query parameters to add to each request. array( param => value )
+        * @param $loadCall boolean If true, output a mw.loader.load() call rather than a <script src="..."> tag
         * @return string html <script> and <style> tags
         */
-       protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array() ) {
+       protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) {
                global $wgResourceLoaderUseESI, $wgResourceLoaderInlinePrivateModules;
 
                if ( !count( $modules ) ) {
@@ -2633,6 +2634,12 @@ $templates
                                // Automatically select style/script elements
                                if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
                                        $link = Html::linkedStyle( $url );
+                               } else if ( $loadCall ) { 
+                                       $link = Html::inlineScript(
+                                               ResourceLoader::makeLoaderConditionalScript(
+                                                       Xml::encodeJsCall( 'mw.loader.load', array( $url ) )
+                                               )
+                                       );
                                } else {
                                        $link = Html::linkedScript( $url );
                                }
@@ -2654,6 +2661,8 @@ $templates
         * @return String: HTML fragment
         */
        function getHeadScripts() {
+               global $wgResourceLoaderExperimentalAsyncLoading;
+               
                // Startup - this will immediately load jquery and mediawiki modules
                $scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
 
@@ -2679,27 +2688,43 @@ $templates
                if ( $modules ) {
                        $scripts .= Html::inlineScript(
                                ResourceLoader::makeLoaderConditionalScript(
-                                       Xml::encodeJsCall( 'mw.loader.load', array( $modules ) )
+                                       "mw.loader.setBlocking( true );\n" .
+                                       Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) .
+                                       "\nmw.loader.setBlocking( false );"
                                )
                        );
                }
+               
+               if ( $wgResourceLoaderExperimentalAsyncLoading ) {
+                       $scripts .= $this->getScriptsForBottomQueue( true );
+               }
 
                return $scripts;
        }
 
        /**
-        * JS stuff to put at the bottom of the <body>: modules marked with position 'bottom',
-        * legacy scripts ($this->mScripts), user preferences, site JS and user JS
+        * JS stuff to put at the 'bottom', which can either be the bottom of the <body>
+        * or the bottom of the <head> depending on $wgResourceLoaderExperimentalAsyncLoading:
+        * modules marked with position 'bottom', legacy scripts ($this->mScripts),
+        * user preferences, site JS and user JS
         *
+        * @param $inHead boolean If true, this HTML goes into the <head>, if false it goes into the <body>
         * @return string
         */
-       function getBottomScripts() {
+       function getScriptsForBottomQueue( $inHead ) {
                global $wgUseSiteJs, $wgAllowUserJs;
 
                // Script and Messages "only" requests marked for bottom inclusion
+               // If we're in the <head>, use load() calls rather than <script src="..."> tags
                // Messages should go first
-               $scripts = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ), ResourceLoaderModule::TYPE_MESSAGES );
-               $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ), ResourceLoaderModule::TYPE_SCRIPTS );
+               $scripts = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ),
+                       ResourceLoaderModule::TYPE_MESSAGES, /* $useESI = */ false, /* $extraQuery = */ array(),
+                       /* $loadCall = */ $inHead
+               );
+               $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ),
+                       ResourceLoaderModule::TYPE_SCRIPTS, /* $useESI = */ false, /* $extraQuery = */ array(),
+                       /* $loadCall = */ $inHead
+               );
 
                // Modules requests - let the client calculate dependencies and batch requests as it likes
                // Only load modules that have marked themselves for loading at the bottom
@@ -2719,7 +2744,9 @@ $templates
 
                // Add site JS if enabled
                if ( $wgUseSiteJs ) {
-                       $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS );
+                       $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS,
+                               /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+                       );
                        if( $this->getUser()->isLoggedIn() ){
                                $userScripts[] = 'user.groups';
                        }
@@ -2732,7 +2759,7 @@ $templates
                                // We're on a preview of a JS subpage
                                // Exclude this page from the user module in case it's in there (bug 26283)
                                $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
-                                       array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
+                                       array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
                                );
                                // Load the previewed JS
                                $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
@@ -2740,14 +2767,30 @@ $templates
                                // Include the user module normally
                                // We can't do $userScripts[] = 'user'; because the user module would end up
                                // being wrapped in a closure, so load it raw like 'site'
-                               $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS );
+                               $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+                                       /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+                               );
                        }
                }
-               $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED );
+               $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED,
+                       /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+               );
 
                return $scripts;
        }
 
+       /**
+        * JS stuff to put at the bottom of the <body>
+        */
+       function getBottomScripts() {
+               global $wgResourceLoaderExperimentalAsyncLoading;
+               if ( !$wgResourceLoaderExperimentalAsyncLoading ) {
+                       return $this->getScriptsForBottomQueue( false );
+               } else {
+                       return '';
+               }
+       }
+
        /**
         * Add one or more variables to be set in mw.config in JavaScript.
         *
index 12684f7..5eed8b8 100644 (file)
@@ -346,8 +346,11 @@ var mw = ( function ( $, undefined ) {
                                queue = [],
                                // List of callback functions waiting for modules to be ready to be called
                                jobs = [],
-                               // Flag inidicating that document ready has occured
+                               // Flag indicating that document ready has occured
                                ready = false,
+                               // Whether we should try to load scripts in a blocking way 
+                               // Set with setBlocking()
+                               blocking = false,
                                // Selector cache for the marker element. Use getMarker() to get/use the marker!
                                $marker = null;
        
@@ -569,15 +572,15 @@ var mw = ( function ( $, undefined ) {
                        }
        
                        /**
-                        * Adds a script tag to the body, either using document.write or low-level DOM manipulation,
-                        * depending on whether document-ready has occured yet.
+                        * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
+                        * depending on whether document-ready has occured yet and whether we are in blocking mode.
                         *
                         * @param src String: URL to script, will be used as the src attribute in the script tag
                         * @param callback Function: Optional callback which will be run when the script is done
                         */
                        function addScript( src, callback ) {
-                               var done = false, script;
-                               if ( ready ) {
+                               var done = false, script, head;
+                               if ( ready || !blocking ) {
                                        // jQuery's getScript method is NOT better than doing this the old-fashioned way
                                        // because jQuery will eval the script's code, and errors will not have sane
                                        // line numbers.
@@ -612,13 +615,18 @@ var mw = ( function ( $, undefined ) {
                                                        }
                                                };
                                        }
-                                       document.body.appendChild( script );
+                                       // IE-safe way of getting the <head> . document.documentElement.head doesn't
+                                       // work in scripts that run in the <head>
+                                       head = document.getElementsByTagName( 'head' )[0];
+                                       // Append to the <body> if available, to the <head> otherwise
+                                       (document.body || head).appendChild( script );
                                } else {
                                        document.write( mw.html.element(
                                                'script', { 'type': 'text/javascript', 'src': src }, ''
                                        ) );
                                        if ( $.isFunction( callback ) ) {
                                                // Document.write is synchronous, so this is called when it's done
+                                               // FIXME: that's a lie. doc.write isn't actually synchronous
                                                callback();
                                        }
                                }
@@ -1221,6 +1229,18 @@ var mw = ( function ( $, undefined ) {
                                                return key;
                                        } );
                                },
+
+                               /**
+                                * Enable or disable blocking. If blocking is enabled and
+                                * document ready has not yet occurred, scripts will be loaded
+                                * in a blocking way (using document.write) rather than
+                                * asynchronously using DOM manipulation
+                                * 
+                                * @param b {Boolean} True to enable blocking, false to disable it
+                                */
+                               setBlocking: function( b ) {
+                                       blocking = b;
+                               },
                
                                /**
                                 * For backwards-compatibility with Squid-cached pages. Loads mw.user