From 88724bb4cb95b1424613a050e9dcf3a9d07e3011 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Mon, 20 Sep 2021 23:12:09 +0100 Subject: [PATCH] performance(frontend): Preload FontAwesome, JS and CSS (#3057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add preloads support to Document class * Add frontend extender for asset preloading * Provide default preloads for FontAwesome * Add tests for preload extender and default preloads * Apply fixes from StyleCI [ci skip] [skip ci] * Fix typo * Fix two more typos 🙃 * Preload core JS and CSS * Apply fixes from StyleCI [ci skip] [skip ci] * Reorder preloads * Remove singular preloads method * Use filesystem disk driver for getting FA font paths * Update test to use full URL * Apply fixes from StyleCI [ci skip] [skip ci] * Address review comment * Apply fixes from StyleCI [ci skip] [skip ci] * Fix typo * Apply fixes from StyleCI [ci skip] [skip ci] * Correct callback wrapping * Update src/Extend/Frontend.php Co-authored-by: Sami Mazouz * Update src/Extend/Frontend.php Co-authored-by: Sami Mazouz * Update src/Extend/Frontend.php * Fix preload extender logic * Convert base FontAwesome preloads into a Singleton * Apply fixes from StyleCI [ci skip] [skip ci] Co-authored-by: luceos Co-authored-by: Sami Mazouz Co-authored-by: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com> Co-authored-by: Alexander Skvortsov --- src/Extend/Frontend.php | 55 +++++++++++ src/Frontend/Document.php | 37 ++++++++ src/Frontend/FrontendServiceProvider.php | 49 ++++++++++ .../extenders/FrontendPreloadTest.php | 93 +++++++++++++++++++ 4 files changed, 234 insertions(+) create mode 100644 tests/integration/extenders/FrontendPreloadTest.php diff --git a/src/Extend/Frontend.php b/src/Extend/Frontend.php index 9484a1213..ff7de2021 100644 --- a/src/Extend/Frontend.php +++ b/src/Extend/Frontend.php @@ -16,6 +16,7 @@ use Flarum\Foundation\ContainerUtil; use Flarum\Foundation\Event\ClearingCache; use Flarum\Frontend\Assets; use Flarum\Frontend\Compiler\Source\SourceCollector; +use Flarum\Frontend\Document; use Flarum\Frontend\Frontend as ActualFrontend; use Flarum\Frontend\RecompileFrontendAssets; use Flarum\Http\RouteCollection; @@ -33,6 +34,7 @@ class Frontend implements ExtenderInterface private $routes = []; private $removedRoutes = []; private $content = []; + private $preloadArrs = []; /** * @param string $frontend: The name of the frontend. @@ -124,11 +126,45 @@ class Frontend implements ExtenderInterface return $this; } + /** + * Adds multiple asset preloads. + * + * The parameter should be an array of preload arrays, or a callable that returns this. + * + * A preload array must contain keys that pertain to the `` tag. + * + * For example, the following will add preload tags for a script and font file: + * ``` + * $frontend->preloads([ + * [ + * 'href' => '/assets/my-script.js', + * 'as' => 'script', + * ], + * [ + * 'href' => '/assets/fonts/my-font.woff2', + * 'as' => 'font', + * 'type' => 'font/woff2', + * 'crossorigin' => '' + * ] + * ]); + * ``` + * + * @param callable|array $preloads + * @return self + */ + public function preloads($preloads): self + { + $this->preloadArrs[] = $preloads; + + return $this; + } + public function extend(Container $container, Extension $extension = null) { $this->registerAssets($container, $this->getModuleName($extension)); $this->registerRoutes($container); $this->registerContent($container); + $this->registerPreloads($container); } private function registerAssets(Container $container, string $moduleName): void @@ -236,6 +272,25 @@ class Frontend implements ExtenderInterface ); } + private function registerPreloads(Container $container): void + { + if (empty($this->preloadArrs)) { + return; + } + + $container->resolving( + "flarum.frontend.$this->frontend", + function (ActualFrontend $frontend, Container $container) { + $frontend->content(function (Document $document) use ($container) { + foreach ($this->preloadArrs as $preloadArr) { + $preloads = is_callable($preloadArr) ? ContainerUtil::wrapCallback($preloadArr, $container)($document) : $preloadArr; + $document->preloads = array_merge($document->preloads, $preloads); + } + }); + } + ); + } + private function getModuleName(?Extension $extension): string { return $extension ? $extension->getId() : 'site-custom'; diff --git a/src/Frontend/Document.php b/src/Frontend/Document.php index f938041c5..d08ab658b 100644 --- a/src/Frontend/Document.php +++ b/src/Frontend/Document.php @@ -122,6 +122,28 @@ class Document implements Renderable */ public $css = []; + /** + * An array of preloaded assets. + * + * Each array item should be an array containing keys that pertain to the + * `` tag. + * + * For example, the following will add a preload tag for a FontAwesome font file: + * ``` + * $this->preloads[] = [ + * 'href' => '/assets/fonts/fa-solid-900.woff2', + * 'as' => 'font', + * 'type' => 'font/woff2', + * 'crossorigin' => '' + * ]; + * ``` + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload + * + * @var array + */ + public $preloads = []; + /** * @var Factory */ @@ -203,6 +225,19 @@ class Document implements Renderable return $this->view->make($this->contentView)->with('content', $this->content); } + protected function makePreloads(): array + { + return array_map(function ($preload) { + $attributes = ''; + + foreach ($preload as $key => $value) { + $attributes .= " $key=\"".e($value).'"'; + } + + return ""; + }, $this->preloads); + } + /** * @return string */ @@ -216,6 +251,8 @@ class Document implements Renderable $head[] = ''; } + $head = array_merge($head, $this->makePreloads()); + $head = array_merge($head, array_map(function ($content, $name) { return ''; }, $this->meta, array_keys($this->meta))); diff --git a/src/Frontend/FrontendServiceProvider.php b/src/Frontend/FrontendServiceProvider.php index cb91a5076..f8cde1689 100644 --- a/src/Frontend/FrontendServiceProvider.php +++ b/src/Frontend/FrontendServiceProvider.php @@ -54,9 +54,58 @@ class FrontendServiceProvider extends AbstractServiceProvider $frontend->content($container->make(Content\CorePayload::class)); $frontend->content($container->make(Content\Meta::class)); + $frontend->content(function (Document $document) use ($container) { + $default_preloads = $container->make('flarum.frontend.default_preloads'); + + // Add preloads for base CSS and JS assets. Extensions should add their own via the extender. + $js_preloads = []; + $css_preloads = []; + + foreach ($document->css as $url) { + $css_preloads[] = [ + 'href' => $url, + 'as' => 'style' + ]; + } + foreach ($document->js as $url) { + $css_preloads[] = [ + 'href' => $url, + 'as' => 'script' + ]; + } + + $document->preloads = array_merge( + $css_preloads, + $js_preloads, + $default_preloads, + $document->preloads, + ); + }); + return $frontend; }; }); + + $this->container->singleton( + 'flarum.frontend.default_preloads', + function (Container $container) { + $filesystem = $container->make('filesystem')->disk('flarum-assets'); + + return [ + [ + 'href' => $filesystem->url('fonts/fa-solid-900.woff2'), + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin' => '' + ], [ + 'href' => $filesystem->url('fonts/fa-regular-400.woff2'), + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin' => '' + ] + ]; + } + ); } /** diff --git a/tests/integration/extenders/FrontendPreloadTest.php b/tests/integration/extenders/FrontendPreloadTest.php new file mode 100644 index 000000000..050cd393f --- /dev/null +++ b/tests/integration/extenders/FrontendPreloadTest.php @@ -0,0 +1,93 @@ +send( + $this->request('GET', '/') + ); + + $filesystem = $this->app()->getContainer()->make('filesystem')->disk('flarum-assets'); + + $urls = [ + $filesystem->url('fonts/fa-solid-900.woff2'), + $filesystem->url('fonts/fa-regular-400.woff2'), + ]; + + $body = $response->getBody()->getContents(); + + foreach ($urls as $url) { + $this->assertStringContainsString("", $body); + } + } + + /** + * @test + */ + public function preloads_can_be_added() + { + $urls = $this->customPreloadUrls; + + $this->extend( + (new Extend\Frontend('forum')) + ->preloads( + array_map(function ($url) { + return ['href' => $url]; + }, $urls) + ) + ); + + $response = $this->send( + $this->request('GET', '/') + ); + $body = $response->getBody()->getContents(); + + foreach ($urls as $url) { + $this->assertStringContainsString("", $body); + } + } + + /** + * @test + */ + public function preloads_can_be_added_via_callable() + { + $urls = $this->customPreloadUrls; + + $this->extend( + (new Extend\Frontend('forum')) + ->preloads(function () use ($urls) { + return array_map(function ($url) { + return ['href' => $url]; + }, $urls); + }) + ); + + $response = $this->send( + $this->request('GET', '/') + ); + $body = $response->getBody()->getContents(); + + foreach ($urls as $url) { + $this->assertStringContainsString("", $body); + } + } +}