From: Gilles Dubuc Date: Mon, 1 Jun 2015 16:58:42 +0000 (+0200) Subject: Preload the logo using link rel="preload" http header X-Git-Tag: 1.31.0-rc.0~3530^2 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=5f55e9c9c2a2416604c6b71a345353b73a960c50 Preload the logo using link rel="preload" http header This greatly increases the priority of loading the logo on browsers that support rel="preload". Bug: T100999 Change-Id: I0738fcc0a575153dab65016fa87faaa9b8b97a9d --- diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 53802647b5..3f10c06ba9 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -302,6 +302,11 @@ class OutputPage extends ContextSource { /** @var array Profiling data */ private $limitReportJSData = []; + /** + * Link: header contents + */ + private $mLinkHeader = []; + /** * Constructor for OutputPage. This should not be called directly. * Instead a new RequestContext should be created and it will implicitly create @@ -2105,6 +2110,28 @@ class OutputPage extends ContextSource { return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) ); } + /** + * Add an HTTP Link: header + * + * @param string $header Header value + */ + public function addLinkHeader( $header ) { + $this->mLinkHeader[] = $header; + } + + /** + * Return a Link: header. Based on the values of $mLinkHeader. + * + * @return string + */ + public function getLinkHeader() { + if ( !$this->mLinkHeader ) { + return false; + } + + return 'Link: ' . implode( ',', $this->mLinkHeader ); + } + /** * Get a complete Key header * @@ -2361,6 +2388,12 @@ class OutputPage extends ContextSource { // jQuery etc. can work correctly. $response->header( 'X-UA-Compatible: IE=Edge' ); + $this->addLogoPreloadLinkHeaders(); + $linkHeader = $this->getLinkHeader(); + if ( $linkHeader ) { + $response->header( $linkHeader ); + } + // Prevent framing, if requested $frameOptions = $this->getFrameOptions(); if ( $frameOptions ) { @@ -3960,4 +3993,82 @@ class OutputPage extends ContextSource { 'mediawiki.widgets.styles', ] ); } + + /** + * Add Link headers for preloading the wiki's logo. + * + * @since 1.26 + */ + protected function addLogoPreloadLinkHeaders() { + $logo = $this->getConfig()->get( 'Logo' ); // wgLogo + $logoHD = $this->getConfig()->get( 'LogoHD' ); // wgLogoHD + + $tags = []; + $logosPerDppx = []; + $logos = []; + + $logosPerDppx['1.0'] = $logo; + + if ( !$logoHD ) { + // No media queries required if we only have one variant + $this->addLinkHeader( '<' . $logo . '>;rel=preload;as=image' ); + return; + } + + foreach ( $logoHD as $dppx => $src ) { + // Only 1.5x and 2x are supported + // Note: Keep in sync with ResourceLoaderSkinModule + if ( in_array( $dppx, [ '1.5x', '2x' ] ) ) { + // LogoHD uses a string in this format: "1.5x" + $dppx = substr( $dppx, 0, -1 ); + $logosPerDppx[$dppx] = $src; + } + } + + // Because PHP can't have floats as array keys + uksort( $logosPerDppx, function ( $a , $b ) { + $a = floatval( $a ); + $b = floatval( $b ); + + if ( $a == $b ) { + return 0; + } + // Sort from smallest to largest (e.g. 1x, 1.5x, 2x) + return ( $a < $b ) ? -1 : 1; + } ); + + foreach ( $logosPerDppx as $dppx => $src ) { + $logos[] = [ 'dppx' => $dppx, 'src' => $src ]; + } + + $logosCount = count( $logos ); + // Logic must match ResourceLoaderSkinModule: + // - 1x applies to resolution < 1.5dppx + // - 1.5x applies to resolution >= 1.5dppx && < 2dppx + // - 2x applies to resolution >= 2dppx + // Note that min-resolution and max-resolution are both inclusive. + for ( $i = 0; $i < $logosCount; $i++ ) { + if ( $i === 0 ) { + // Smallest dppx + // min-resolution is ">=" (larger than or equal to) + // "not min-resolution" is essentially "<" + $media_query = 'not all and (min-resolution: ' . $logos[ 1 ]['dppx'] . 'dppx)'; + } elseif ( $i !== $logosCount - 1 ) { + // In between + // Media query expressions can only apply "not" to the entire expression + // (e.g. can't express ">= 1.5 and not >= 2). + // Workaround: Use <= 1.9999 in place of < 2. + $upper_bound = floatval( $logos[ $i + 1 ]['dppx'] ) - 0.000001; + $media_query = '(min-resolution: ' . $logos[ $i ]['dppx'] . + 'dppx) and (max-resolution: ' . $upper_bound . 'dppx)'; + } else { + // Largest dppx + $media_query = '(min-resolution: ' . $logos[ $i ]['dppx'] . 'dppx)'; + } + + $this->addLinkHeader( + '<' . $logos[$i]['src'] . '>;rel=preload;as=image;media=' . $media_query + ); + } + } } diff --git a/includes/resourceloader/ResourceLoaderSkinModule.php b/includes/resourceloader/ResourceLoaderSkinModule.php index d72b3afaf5..7d37944f0e 100644 --- a/includes/resourceloader/ResourceLoaderSkinModule.php +++ b/includes/resourceloader/ResourceLoaderSkinModule.php @@ -40,6 +40,8 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule { $styles['all'][] = '.mw-wiki-logo { background-image: ' . CSSMin::buildUrlValue( $logo1 ) . '; }'; + // Only 1.5x and 2x are supported + // Note: Keep in sync with OutputPage::addLogoPreloadLinkHeaders() if ( $logoHD ) { if ( isset( $logoHD['1.5x'] ) ) { $styles[ diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php index 59441ce08a..7571e261d4 100644 --- a/tests/phpunit/includes/OutputPageTest.php +++ b/tests/phpunit/includes/OutputPageTest.php @@ -472,13 +472,93 @@ class OutputPageTest extends MediaWikiTestCase { $this->assertEquals( [ 0 => 'Test' ], $outputPage->getCategories( 'hidden' ) ); } + /** + * @dataProvider provideLinkHeaders + * @covers OutputPage::addLinkHeader + * @covers OutputPage::getLinkHeader + */ + public function testLinkHeaders( $headers, $result ) { + $outputPage = $this->newInstance(); + + foreach ( $headers as $header ) { + $outputPage->addLinkHeader( $header ); + } + + $this->assertEquals( $result, $outputPage->getLinkHeader() ); + } + + public function provideLinkHeaders() { + return [ + [ + [], + false + ], + [ + [ ';rel=preload;as=image' ], + 'Link: ;rel=preload;as=image', + ], + [ + [ ';rel=preload;as=image',';rel=preload;as=image' ], + 'Link: ;rel=preload;as=image,;rel=preload;as=image', + ], + ]; + } + + /** + * @dataProvider providePreloadLinkHeaders + * @covers OutputPage::addLogoPreloadLinkHeaders + */ + public function testPreloadLinkHeaders( $config, $result ) { + $out = TestingAccessWrapper::newFromObject( $this->newInstance( $config ) ); + $out->addLogoPreloadLinkHeaders(); + + $this->assertEquals( $result, $out->getLinkHeader() ); + } + + public function providePreloadLinkHeaders() { + return [ + [ + [ + 'Logo' => '/img/default.png', + 'LogoHD' => [ + '1.5x' => '/img/one-point-five.png', + '2x' => '/img/two-x.png', + ], + ], + 'Link: ;rel=preload;as=image;media=' . + 'not all and (min-resolution: 1.5dppx),' . + ';rel=preload;as=image;media=' . + '(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' . + ';rel=preload;as=image;media=(min-resolution: 2dppx)' + ], + [ + [ + 'Logo' => '/img/default.png', + 'LogoHD' => false, + ], + 'Link: ;rel=preload;as=image' + ], + [ + [ + 'Logo' => '/img/default.png', + 'LogoHD' => [ + '2x' => '/img/two-x.png', + ], + ], + 'Link: ;rel=preload;as=image;media=' . + 'not all and (min-resolution: 2dppx),' . + ';rel=preload;as=image;media=(min-resolution: 2dppx)' + ], + ]; + } + /** * @return OutputPage */ - private function newInstance() { + private function newInstance( $config = [] ) { $context = new RequestContext(); - $context->setConfig( new HashConfig( [ + $context->setConfig( new HashConfig( $config + [ 'AppleTouchIcon' => false, 'DisableLangConversion' => true, 'EnableAPI' => false,