使用 Meteor 在 Google PageSpeed Insights 上得分 100(即网络应用登陆页面)
Score 100 on Google PageSpeed Insights using Meteor (ie. a web-app landing page)
我的客户要求功能丰富的客户端呈现网络应用程序,同时在 Google PageSpeed Insights 上得分为 100/100,并且在第一次加载时呈现速度非常快,缓存为空。她想将同一个网站用作网络应用程序和登录页面,并让任何搜索引擎轻松抓取具有良好 SEO 的整个网站。
这可以使用 Meteor 吗?怎么做到的?
是的,使用 Meteor 1.3、一些额外的软件包和一个小技巧,这很简单os。
有关示例,请参阅 bc-real-estate-math.com。 (这个网站只有 97 分,因为我没有调整图片的大小,而且 Analytics 和 FB 跟踪的缓存寿命很短)
传统上,像 Meteor 这样的 client-side 渲染平台在第一次加载空缓存时速度很慢,因为 Javascript 负载很大。 Server-side 渲染(使用 React)第一页 almost 解决了这个问题,除了 Meteor out-of-the-box 不支持异步 Javascript 或内联 CSS 从而减慢速度您的第一个渲染并杀死您的 Google PageSpeed Insights 分数(并且您可能会争论该指标,它会影响我客户的 AdWord 价格,因此我会针对它进行优化)。
这是您可以通过此答案的设置实现的:
- 在空缓存上非常快 time-to-first-render,大约 500 毫秒
- 没有"flash of styled content"
- Google PageSpeed Insights 得分 100/100
- 在不破坏您的 PageSpeed 分数的情况下使用任何网络字体
- 包括页面标题和元数据在内的完整 SEO 控制
- 与 Google 分析和 Facebook 像素的完美集成
准确记录每个页面视图,无论服务器或
client-side渲染
- Google 搜索机器人和其他抓取工具无需 运行 脚本
即可立即查看您所有页面的真实 HTML
- 无缝处理 #hash URL 以滚动到页面的某些部分
- 使用少量(如 < 30)图标字体
不添加请求或损害速度得分的字符
- 在不影响着陆页体验的情况下扩展到 Javascript 的任意大小
- 完整流星的所有常规威力web-app
此设置无法实现的内容:
- 大型单体 CSS 框架将开始扼杀您的 PageSpeed 分数并减慢 time-to-first-render。 Bootstrap 在您开始看到问题之前,您可以做到最大程度
- 您无法避免 flash-of-wrong-font 并仍然保持 100/100 PageSpeed。第一个渲染将是客户端的 web-safe 字体,第二个渲染将使用您之前推迟的任何字体。
基本上你可以实现的是:
- 客户请求您站点内的任何 url
- 服务器发回一个
使用内联 CSS、异步 Javascript 和延迟完成 HTML 文件
字体
- 客户端请求图像(如果有),服务器发送它们
- 客户端现在可以呈现页面
- 延迟字体(如果有)到达并且页面可能 re-render
- Javascript母舰有效载荷到达
背景
- Meteor 启动,你有一个功能齐全的 web-app
所有的花里胡哨,没有 first-load 惩罚
- 只要你
给用户几行文字和一张漂亮的图片
看,他们永远不会注意到静态 HTML 的转换
页面到 full-blown web-app
如何完成这个
我使用了 Meteor 1.3 和这些附加包:
- 反应
- react-dom
- react-router
- react-router-ssr
- react-helmet
- postcss
- 自动前缀
- meteor-node-stubs
React 与 server-side 渲染配合得很好,我还没有尝试过任何其他渲染引擎。 react-helmet 用于轻松添加和修改客户端和 server-side 每个页面的 <head>
(例如,需要设置每个页面的标题)。我使用 autoprefixer 将所有 vendor-specific 前缀添加到我的 CSS/SASS,当然本练习不需要。
按照 react-router、reac-router-ssr 和 react-helmet 文档中的示例,站点的 Most 非常简单。有关详细信息,请参阅 os 软件包的文档。
首先,一个非常重要的文件应该在共享的 Meteor 目录中(即不在服务器或客户端文件夹中)。此代码设置 React server-side 渲染、<head>
标签、Google 分析、Facebook 跟踪,并滚动到#hash 锚点。
import { Meteor } from 'meteor/meteor';
import { ReactRouterSSR } from 'meteor/reactrouter:react-router-ssr';
import { Routes } from '../imports/startup/routes.jsx';
import Helmet from 'react-helmet';
ReactRouterSSR.Run(
Routes,
{
props: {
onUpdate() {
hashLinkScroll();
// Notify the page has been changed to Google Analytics
ga('send', 'pageview');
},
htmlHook(html) {
const head = Helmet.rewind();
html = html.replace('<head>', '<head>' + head.title + head.base + head.meta + head.link + head.script);
return html; }
}
},
{
htmlHook(html){
const head = Helmet.rewind();
html = html.replace('<head>', '<head>' + head.title + head.base + head.meta + head.link + head.script);
return html;
},
}
);
if(Meteor.isClient){
// Google Analytics
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-xxxxx-1', 'auto', {'allowLinker': true});
ga('require', 'linker');
ga('linker:autoLink', ['another-domain.com']);
ga('send', 'pageview');
// Facebook tracking
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'xxxx');
fbq('track', "PageView");
fbq('trackCustom', 'LoggedOutPageView');
}
function hashLinkScroll() {
const { hash } = window.location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(() => {
$('html, body').animate({
scrollTop: $(hash).offset().top
}, 1000);
}, 100);
}
}
这是路线的设置方式。请注意稍后馈送到 react-helmet 以设置 <head>
内容的标题属性。
import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../ui/App.jsx';
import Homepage from '../ui/pages/Homepage.jsx';
import ExamTips from '../ui/pages/ExamTips.jsx';
export const Routes = (
<Route path="/" component={App}>
<IndexRoute
displayTitle="BC Real Estate Math Online Course"
pageTitle="BC Real Estate Math Online Course"
isHomepage
component={Homepage} />
<Route path="exam-preparation-and-tips">
<Route
displayTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam"
pageTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam"
path="top-math-mistakes-to-avoid"
component={ExamTips} />
</Route>
);
App.jsx--外部应用组件。请注意 <Helmet>
标记,它根据特定页面组件的属性设置一些元标记和页面标题。
import React, { Component } from 'react';
import { Link } from 'react-router';
import Helmet from "react-helmet";
export default class App extends Component {
render() {
return (
<div className="site-wrapper">
<Helmet
title={this.props.children.props.route.pageTitle}
meta={[
{name: 'viewport', content: 'width=device-width, initial-scale=1'},
]}
/>
<nav className="site-nav">...
示例页面组件:
import React, { Component } from 'react';
import { Link } from 'react-router';
export default class ExamTips extends Component {
render() {
return (
<div className="exam-tips blog-post">
<section className="intro">
<p>
...
如何添加延迟字体。
这些字体将在初始渲染后加载,因此不会延迟 time-to-first-render。我相信这是在不降低 PageSpeed 分数的情况下使用 webfonts 的唯一方法。然而,它确实导致了一个简短的 flash-of-wrong-font。将其放入客户端包含的脚本文件中:
WebFontConfig = {
google: { families: [ 'Open+Sans:400,300,300italic,400italic,700:latin' ] }
};
(function() {
var wf = document.createElement('script');
wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
})();
如果您使用像 fontello.com 和 hand-pick 这样的优质服务,只有您真正需要的图标,您可以将它们嵌入到您的内联 <head>
CSS 中并获取图标冷杉无需等待大字体文件即可呈现。
黑客
这还不够 os 但问题是我们的脚本 CSS 和字体正在同步加载并减慢渲染速度并破坏我们的 PageSpeed 分数。不幸的是,据我所知,Meteor 1.3 不正式支持任何内联 CSS 或将 async 属性添加到脚本标签的方法。我们必须破解核心 boilerplate-generator 包的 3 个文件中的几行。
~/.meteor/packages/boilerplate-generator/.1.0.8.4n62e6++os+web.browser+web.cordova/os/boilerplate-generator.js
...
Boilerplate.prototype._generateBoilerplateFromManifestAndSource =
function (manifest, boilerplateSource, options) {
var self = this;
// map to the identity by default
var urlMapper = options.urlMapper || _.identity;
var pathMapper = options.pathMapper || _.identity;
var boilerplateBaseData = {
css: [],
js: [],
head: '',
body: '',
meteorManifest: JSON.stringify(manifest),
jsAsyncAttr: Meteor.isProduction?'async':null, // <------------ !!
};
....
if (item.type === 'css' && item.where === 'client') {
if(Meteor.isProduction){ // <------------ !!
// Get the contents of aggregated and minified CSS files as a string
itemObj.inlineStyles = fs.readFileSync(pathMapper(item.path), "utf8");;
itemObj.inline = true;
}
boilerplateBaseData.css.push(itemObj);
}
...
~/.meteor/packages/boilerplate-generator/.1.0.8.4n62e6++os+web.browser+web.cordova/os/packages/boilerplate-generator/boilerplate_web.browser.html
<html {{htmlAttributes}}>
<head>
{{#each css}}
{{#if inline}}
<style>{{{inlineStyles}}}</style>
{{else}}
<link rel="stylesheet" type="text/css" class="__meteor-css__" href="{{../bundledJsCssUrlRewriteHook url}}">
{{/if}}
{{/each}}
{{{head}}}
{{{dynamicHead}}}
</head>
<body>
{{{body}}}
{{{dynamicBody}}}
{{#if inlineScriptsAllowed}}
<script type='text/javascript'>__meteor_runtime_config__ = JSON.parse(decodeURIComponent({{meteorRuntimeConfig}}));</script>
{{else}}
<script {{../jsAsyncAttr}} type='text/javascript' src='{{rootUrlPathPrefix}}/meteor_runtime_config.js'></script>
{{/if}}
{{#each js}}
<script {{../jsAsyncAttr}} type="text/javascript" src="{{../bundledJsCssUrlRewriteHook url}}"></script>
{{/each}}
{{#each additionalStaticJs}}
{{#if ../inlineScriptsAllowed}}
<script type='text/javascript'>
{{contents}}
</script>
{{else}}
<script {{../jsAsyncAttr}} type='text/javascript' src='{{rootUrlPathPrefix}}{{pathname}}'></script>
{{/if}}
{{/each}}
</body>
</html>
现在计算您编辑的ose 2 个文件中的字符数,并在中第ose 个文件条目的长度字段中输入新值~ /.meteor/packages/boilerplate-generator/.1.0.8.4n62e6++os+web.browser+web.cordova/os.json
然后删除project/.meteor/local文件夹强制Meteor使用新的核心包并重启你的应用程序(热重载将不起作用)。您只会看到生产模式的变化。
这显然是一个 hack,会在 Meteor 更新时中断。我希望通过 pos 发布并引起一些兴趣,我们将朝着更好的方向努力。
待办事项
需要改进的地方是:
- 避免黑客攻击。让 MDG 正式支持异步脚本和内联
CSS 灵活的方式
- 允许精细控制 CSS 内联和延迟
- 允许精细控制哪些 JS 异步,哪些同步
要内联的。
我的客户要求功能丰富的客户端呈现网络应用程序,同时在 Google PageSpeed Insights 上得分为 100/100,并且在第一次加载时呈现速度非常快,缓存为空。她想将同一个网站用作网络应用程序和登录页面,并让任何搜索引擎轻松抓取具有良好 SEO 的整个网站。
这可以使用 Meteor 吗?怎么做到的?
是的,使用 Meteor 1.3、一些额外的软件包和一个小技巧,这很简单os。
有关示例,请参阅 bc-real-estate-math.com。 (这个网站只有 97 分,因为我没有调整图片的大小,而且 Analytics 和 FB 跟踪的缓存寿命很短)
传统上,像 Meteor 这样的 client-side 渲染平台在第一次加载空缓存时速度很慢,因为 Javascript 负载很大。 Server-side 渲染(使用 React)第一页 almost 解决了这个问题,除了 Meteor out-of-the-box 不支持异步 Javascript 或内联 CSS 从而减慢速度您的第一个渲染并杀死您的 Google PageSpeed Insights 分数(并且您可能会争论该指标,它会影响我客户的 AdWord 价格,因此我会针对它进行优化)。
这是您可以通过此答案的设置实现的:
- 在空缓存上非常快 time-to-first-render,大约 500 毫秒
- 没有"flash of styled content"
- Google PageSpeed Insights 得分 100/100
- 在不破坏您的 PageSpeed 分数的情况下使用任何网络字体
- 包括页面标题和元数据在内的完整 SEO 控制
- 与 Google 分析和 Facebook 像素的完美集成 准确记录每个页面视图,无论服务器或 client-side渲染
- Google 搜索机器人和其他抓取工具无需 运行 脚本 即可立即查看您所有页面的真实 HTML
- 无缝处理 #hash URL 以滚动到页面的某些部分
- 使用少量(如 < 30)图标字体 不添加请求或损害速度得分的字符
- 在不影响着陆页体验的情况下扩展到 Javascript 的任意大小
- 完整流星的所有常规威力web-app
此设置无法实现的内容:
- 大型单体 CSS 框架将开始扼杀您的 PageSpeed 分数并减慢 time-to-first-render。 Bootstrap 在您开始看到问题之前,您可以做到最大程度
- 您无法避免 flash-of-wrong-font 并仍然保持 100/100 PageSpeed。第一个渲染将是客户端的 web-safe 字体,第二个渲染将使用您之前推迟的任何字体。
基本上你可以实现的是:
- 客户请求您站点内的任何 url
- 服务器发回一个 使用内联 CSS、异步 Javascript 和延迟完成 HTML 文件 字体
- 客户端请求图像(如果有),服务器发送它们
- 客户端现在可以呈现页面
- 延迟字体(如果有)到达并且页面可能 re-render
- Javascript母舰有效载荷到达 背景
- Meteor 启动,你有一个功能齐全的 web-app 所有的花里胡哨,没有 first-load 惩罚
- 只要你 给用户几行文字和一张漂亮的图片 看,他们永远不会注意到静态 HTML 的转换 页面到 full-blown web-app
如何完成这个
我使用了 Meteor 1.3 和这些附加包:
- 反应
- react-dom
- react-router
- react-router-ssr
- react-helmet
- postcss
- 自动前缀
- meteor-node-stubs
React 与 server-side 渲染配合得很好,我还没有尝试过任何其他渲染引擎。 react-helmet 用于轻松添加和修改客户端和 server-side 每个页面的 <head>
(例如,需要设置每个页面的标题)。我使用 autoprefixer 将所有 vendor-specific 前缀添加到我的 CSS/SASS,当然本练习不需要。
Most 非常简单。有关详细信息,请参阅 os 软件包的文档。
首先,一个非常重要的文件应该在共享的 Meteor 目录中(即不在服务器或客户端文件夹中)。此代码设置 React server-side 渲染、<head>
标签、Google 分析、Facebook 跟踪,并滚动到#hash 锚点。
import { Meteor } from 'meteor/meteor';
import { ReactRouterSSR } from 'meteor/reactrouter:react-router-ssr';
import { Routes } from '../imports/startup/routes.jsx';
import Helmet from 'react-helmet';
ReactRouterSSR.Run(
Routes,
{
props: {
onUpdate() {
hashLinkScroll();
// Notify the page has been changed to Google Analytics
ga('send', 'pageview');
},
htmlHook(html) {
const head = Helmet.rewind();
html = html.replace('<head>', '<head>' + head.title + head.base + head.meta + head.link + head.script);
return html; }
}
},
{
htmlHook(html){
const head = Helmet.rewind();
html = html.replace('<head>', '<head>' + head.title + head.base + head.meta + head.link + head.script);
return html;
},
}
);
if(Meteor.isClient){
// Google Analytics
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-xxxxx-1', 'auto', {'allowLinker': true});
ga('require', 'linker');
ga('linker:autoLink', ['another-domain.com']);
ga('send', 'pageview');
// Facebook tracking
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'xxxx');
fbq('track', "PageView");
fbq('trackCustom', 'LoggedOutPageView');
}
function hashLinkScroll() {
const { hash } = window.location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(() => {
$('html, body').animate({
scrollTop: $(hash).offset().top
}, 1000);
}, 100);
}
}
这是路线的设置方式。请注意稍后馈送到 react-helmet 以设置 <head>
内容的标题属性。
import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../ui/App.jsx';
import Homepage from '../ui/pages/Homepage.jsx';
import ExamTips from '../ui/pages/ExamTips.jsx';
export const Routes = (
<Route path="/" component={App}>
<IndexRoute
displayTitle="BC Real Estate Math Online Course"
pageTitle="BC Real Estate Math Online Course"
isHomepage
component={Homepage} />
<Route path="exam-preparation-and-tips">
<Route
displayTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam"
pageTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam"
path="top-math-mistakes-to-avoid"
component={ExamTips} />
</Route>
);
App.jsx--外部应用组件。请注意 <Helmet>
标记,它根据特定页面组件的属性设置一些元标记和页面标题。
import React, { Component } from 'react';
import { Link } from 'react-router';
import Helmet from "react-helmet";
export default class App extends Component {
render() {
return (
<div className="site-wrapper">
<Helmet
title={this.props.children.props.route.pageTitle}
meta={[
{name: 'viewport', content: 'width=device-width, initial-scale=1'},
]}
/>
<nav className="site-nav">...
示例页面组件:
import React, { Component } from 'react';
import { Link } from 'react-router';
export default class ExamTips extends Component {
render() {
return (
<div className="exam-tips blog-post">
<section className="intro">
<p>
...
如何添加延迟字体。
这些字体将在初始渲染后加载,因此不会延迟 time-to-first-render。我相信这是在不降低 PageSpeed 分数的情况下使用 webfonts 的唯一方法。然而,它确实导致了一个简短的 flash-of-wrong-font。将其放入客户端包含的脚本文件中:
WebFontConfig = {
google: { families: [ 'Open+Sans:400,300,300italic,400italic,700:latin' ] }
};
(function() {
var wf = document.createElement('script');
wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
})();
如果您使用像 fontello.com 和 hand-pick 这样的优质服务,只有您真正需要的图标,您可以将它们嵌入到您的内联 <head>
CSS 中并获取图标冷杉无需等待大字体文件即可呈现。
黑客
这还不够 os 但问题是我们的脚本 CSS 和字体正在同步加载并减慢渲染速度并破坏我们的 PageSpeed 分数。不幸的是,据我所知,Meteor 1.3 不正式支持任何内联 CSS 或将 async 属性添加到脚本标签的方法。我们必须破解核心 boilerplate-generator 包的 3 个文件中的几行。
~/.meteor/packages/boilerplate-generator/.1.0.8.4n62e6++os+web.browser+web.cordova/os/boilerplate-generator.js
...
Boilerplate.prototype._generateBoilerplateFromManifestAndSource =
function (manifest, boilerplateSource, options) {
var self = this;
// map to the identity by default
var urlMapper = options.urlMapper || _.identity;
var pathMapper = options.pathMapper || _.identity;
var boilerplateBaseData = {
css: [],
js: [],
head: '',
body: '',
meteorManifest: JSON.stringify(manifest),
jsAsyncAttr: Meteor.isProduction?'async':null, // <------------ !!
};
....
if (item.type === 'css' && item.where === 'client') {
if(Meteor.isProduction){ // <------------ !!
// Get the contents of aggregated and minified CSS files as a string
itemObj.inlineStyles = fs.readFileSync(pathMapper(item.path), "utf8");;
itemObj.inline = true;
}
boilerplateBaseData.css.push(itemObj);
}
...
~/.meteor/packages/boilerplate-generator/.1.0.8.4n62e6++os+web.browser+web.cordova/os/packages/boilerplate-generator/boilerplate_web.browser.html
<html {{htmlAttributes}}>
<head>
{{#each css}}
{{#if inline}}
<style>{{{inlineStyles}}}</style>
{{else}}
<link rel="stylesheet" type="text/css" class="__meteor-css__" href="{{../bundledJsCssUrlRewriteHook url}}">
{{/if}}
{{/each}}
{{{head}}}
{{{dynamicHead}}}
</head>
<body>
{{{body}}}
{{{dynamicBody}}}
{{#if inlineScriptsAllowed}}
<script type='text/javascript'>__meteor_runtime_config__ = JSON.parse(decodeURIComponent({{meteorRuntimeConfig}}));</script>
{{else}}
<script {{../jsAsyncAttr}} type='text/javascript' src='{{rootUrlPathPrefix}}/meteor_runtime_config.js'></script>
{{/if}}
{{#each js}}
<script {{../jsAsyncAttr}} type="text/javascript" src="{{../bundledJsCssUrlRewriteHook url}}"></script>
{{/each}}
{{#each additionalStaticJs}}
{{#if ../inlineScriptsAllowed}}
<script type='text/javascript'>
{{contents}}
</script>
{{else}}
<script {{../jsAsyncAttr}} type='text/javascript' src='{{rootUrlPathPrefix}}{{pathname}}'></script>
{{/if}}
{{/each}}
</body>
</html>
现在计算您编辑的ose 2 个文件中的字符数,并在中第ose 个文件条目的长度字段中输入新值~ /.meteor/packages/boilerplate-generator/.1.0.8.4n62e6++os+web.browser+web.cordova/os.json
然后删除project/.meteor/local文件夹强制Meteor使用新的核心包并重启你的应用程序(热重载将不起作用)。您只会看到生产模式的变化。
这显然是一个 hack,会在 Meteor 更新时中断。我希望通过 pos 发布并引起一些兴趣,我们将朝着更好的方向努力。
待办事项
需要改进的地方是:
- 避免黑客攻击。让 MDG 正式支持异步脚本和内联 CSS 灵活的方式
- 允许精细控制 CSS 内联和延迟
- 允许精细控制哪些 JS 异步,哪些同步 要内联的。