Repository: ProjectEvergreen/greenwood
Pull request #396 (2020-07-21 23:02:55)

Rfc/issue 354 strict / SSG mode
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
in support of #354, introduces a "strict" mode that strips out all client side JavaScript for projects that don't need any "runtime" JavaScript

## Summary of Changes
> Note: #354 will have to stay open, this is not the final solution I'm looking for.  Ideally, there would be a **progressive** mode that would have to be a Babel plugin or something that would only ship the needed "runtime" code, like the banner for the Greenwood website

1. Introduced modes configuration option 
    - **ssg** - ships NO javascript (has some caveats)
    - **spa** - current behavior 
1. **spa** mode marks the `<script>` tag as `async`

Note this neat trick with [inline `<script>` tag](https://github.com/ProjectEvergreen/greenwood/pull/401)

> Posted some initial performance results [here](https://github.com/ProjectEvergreen/greenwood/pull/396#issuecomment-662150746).  In both cases, performance is improved, even if Greenwood website's performance score is low.  The hope is that enabling **progressive** mode could solve this, but probably an issue just for Lighthouse score should be ,ade.

## Questions
1. [x] Should we change the name to something other than mode, which [we might need for this](https://github.com/ProjectEvergreen/greenwood/issues/199)?
    - yes, will call it **optimization**

## TODO
1. [x] initial SPA benchmark / performance report
1. [x] rename **ssg** to **strict** mode (think of another name for **spa** mode?)
1. [x] Make ~~**strict**~~ **spa** mode default (the long term hope is that **progressive** mode would be the new default)
1. [x] Testing
    - [x]  **strict** as default for smoke test and test cases
    - [x] error case
    - [x] add unit tests and update `Config` query
    - [x] add test case for **spa** mode
1. [x] add documentation
1. [x] Refactoring
    - [x] Use [`async` or `defer`](https://discourse.mozilla.org/t/async-v-s-defer/53819/4)?  I think in our case `defer` would work well for us?  (avoid jankiness while hydrating mid load)
      - [x] See if **html-webpack-plugin** can do `async` setting?  (Not sure if `async`/ `defer` will work when serializing though...)
      - [x] page bundles are included as `<script>` tags in index.html - should be `async` too, or removed in **strict** mode?
    - [x] Don't render `*.bundle.js` script tags in **strict** mode
    - [x] Don't render `<lit-redux-router>` in **strict** mode (shaved off 5KB from _index.html_ for Greenwood website!)
1. [x] Rename from **mode** to **optimization**
1. [x] Optimize the 404 page (nice to have, follow up issue) - done
1. [x] Final benchmark / performance report for both modes
    - Strict 
    - SPA
1. [x] Create an issue for tracking getting performance close to 100 for Greenwood / all users - made #411
   -  ~~Would like to look more into why our index bundle is so large or is it as optimized as it can be, for SPA mode.  (maybe split out the webpack runtime?  Might include reviewing the merits of our default babel config too.~~
   -  ~~related issues:~~ #357 , #321, #305, #235
1. [x] Explore an issue for migrating Greenwood website to **strict** mode - #410 

thescientist13: Here are some initial benchmarks from local testing in Greenwood and my personal website.  Will complete with some stats of current performance in productions

## www.greenhouse.io
### Before
TODO

### After
![greenhouse io-ssg-local-network-js](https://user-images.githubusercontent.com/895923/88115820-0e58a200-cb85-11ea-8c7e-d2595a1a1d37.png)
![greenhouse io-ssg-local-lighthouse](https://user-images.githubusercontent.com/895923/88115822-0ef13880-cb85-11ea-8d53-7cd47ca19d3e.png)
![greenhouse io-ssg-local-network](https://user-images.githubusercontent.com/895923/88115817-0d277500-cb85-11ea-96c6-48cca3da19e2.png)


## Greenwood
TODO




thescientist13: A couple things that are also interesting to observe is that production is already pretty bad, but none of the things having to do with the size of our JavaScript will fix things, seemingly?
> These suggestions can help your page load faster. They don't directly affect the Performance score.

The issues with LCP, TTI, TBT are seemingly a result of other causes then?

Naturally, strict / ssg mode passes with flying colors as can be seen in the above comment.

For comparison though, my website is doing somewhat better than greenwood production website though
![greenhouseio-network](https://user-images.githubusercontent.com/895923/88463942-9e982f00-ce84-11ea-8f2d-1ebecf969e1a.png)
![greenhouseio-lighthouse](https://user-images.githubusercontent.com/895923/88463946-a2c44c80-ce84-11ea-8789-75046a4fd28a.png)

Interestingly, the Getting Started repo, which is bare bones, fares surprisingly better out of all of them?  (Which I guess is expected, but why such a sharp drop off?)
![getting-started-network](https://user-images.githubusercontent.com/895923/88463953-af48a500-ce84-11ea-835e-ba58705b2e73.png)
![getting-started-lighthouse](https://user-images.githubusercontent.com/895923/88463960-c12a4800-ce84-11ea-9fe9-e8e58b36dd47.png)

-----

So perhaps the issues here are related to 
- something specific to our app code that isn't being done in the Getting Started? 
- would be good to see if commenting out some of the bundled code would help improve the score?  (like if bundle size was closer to 100KB?)
- Maybe we should use more `HTMLElement` where possible?
hutchgrant: * comment out external font from `style/theme.css`
* comment out  analytics, polyfills, markdown plugins from `greenwood.config.js`

In spa mode I noticed an immediate reduction in **Total Blocking Time** when testing lighthouse in mobile mode (where the problem is).  Which resulted in an increase score of performance 67.  Not great but an improvement none the less.

in ssg mode those changes resulted in a performance score of 100 when testing lighting house in mobile mode.




hutchgrant: nearly a perfect lighthouse score on mobile and desktop. One issue I noticed, our "get started" button is now green again.

Patch:
diff --git a/packages/cli/src/data/client.js b/packages/cli/src/data/client.js
index f5d99297..f562b339 100644
--- a/packages/cli/src/data/client.js
+++ b/packages/cli/src/data/client.js
@@ -14,7 +14,7 @@ const backupQuery = client.query;
 
 client.query = (params) => {
   if (APOLLO_STATE) {
-    // __APOLLO_STATE__ defined, in "SSG" mode...
+    // __APOLLO_STATE__ defined, in production mode
     const queryHash = getQueryHash(params.query, params.variables);
     const cachePath = `/${queryHash}-cache.json`;
     
@@ -27,7 +27,7 @@ client.query = (params) => {
         };
       });
   } else {
-    // __APOLLO_STATE__ NOT defined, in "SPA" mode
+    // __APOLLO_STATE__ NOT defined, in development mode
     return backupQuery(params);
   }
 };
diff --git a/packages/cli/src/data/queries/config.gql b/packages/cli/src/data/queries/config.gql
index 371cc886..24d57c5f 100644
--- a/packages/cli/src/data/queries/config.gql
+++ b/packages/cli/src/data/queries/config.gql
@@ -12,6 +12,7 @@ query {
       value,
       href
     },
+    optimization,
     publicPath,
     title,
     workspace
diff --git a/packages/cli/src/data/schema/config.js b/packages/cli/src/data/schema/config.js
index a4adea9f..7c715ba9 100644
--- a/packages/cli/src/data/schema/config.js
+++ b/packages/cli/src/data/schema/config.js
@@ -23,6 +23,7 @@ const configTypeDefs = gql`
   type Config {
     devServer: DevServer,
     meta: [Meta],
+    optimization: String,
     publicPath: String,
     title: String,
     workspace: String
diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js
index 93c70ff1..d9bad00b 100644
--- a/packages/cli/src/lifecycles/config.js
+++ b/packages/cli/src/lifecycles/config.js
@@ -2,12 +2,15 @@ const fs = require('fs-extra');
 const path = require('path');
 const url = require('url');
 
+const optimizations = ['strict', 'spa'];
+
 let defaultConfig = {
   workspace: path.join(process.cwd(), 'src'),
   devServer: {
     port: 1984,
     host: 'localhost'
   },
+  optimization: 'spa',
   publicPath: '/',
   title: 'My App',
   meta: [],
@@ -25,7 +28,7 @@ module.exports = readAndMergeConfig = async() => {
 
       if (await fs.exists(path.join(process.cwd(), 'greenwood.config.js'))) {
         const userCfgFile = require(path.join(process.cwd(), 'greenwood.config.js'));
-        const { workspace, devServer, publicPath, title, meta, plugins, themeFile, markdown } = userCfgFile;
+        const { workspace, devServer, optimization, publicPath, title, meta, plugins, themeFile, markdown } = userCfgFile;
 
         // workspace validation
         if (workspace) {
@@ -72,6 +75,12 @@ module.exports = readAndMergeConfig = async() => {
           customConfig.meta = meta;
         }
 
+        if (typeof optimization === 'string' && optimizations.indexOf(optimization.toLowerCase()) >= 0) {
+          customConfig.optimization = optimization;
+        } else if (optimization) {
+          reject(`Error: provided optimization "${optimization}" is not supported.  Please use one of: ${optimizations.join(', ')}.`);
+        }
+
         if (plugins && plugins.length > 0) {
           const types = ['index', 'webpack'];
 
diff --git a/packages/cli/src/lifecycles/serialize.js b/packages/cli/src/lifecycles/serialize.js
index 52c4ff22..ce9bc3ed 100644
--- a/packages/cli/src/lifecycles/serialize.js
+++ b/packages/cli/src/lifecycles/serialize.js
@@ -21,13 +21,27 @@ module.exports = serializeBuild = async (compilation) => {
   
         return await browserRunner.serialize(`http://127.0.0.1:${PORT}${route}`).then(async (content) => {
           const target = path.join(publicDir, route);
-          const html = content
-            .replace(polyfill, '')
-            .replace('<script></script>', `
-              <script data-state="apollo">
+          const optimization = compilation.config.optimization;
+          const isStrictOptimization = optimization === 'strict';
+          const apolloScript = isStrictOptimization
+            ? ''
+            : `<script data-state="apollo">
                 window.__APOLLO_STATE__ = true;
               </script> 
-            `);
+            `;
+          
+          let html = content
+            .replace(polyfill, '')
+            .replace('<script></script>', apolloScript);
+
+          if (isStrictOptimization) { // no javascript
+            html = html.replace(/<script type="text\/javascript" src="\/index.*.bundle\.js"><\/script>/, ''); // main bundle
+            html = html.replace(/<script charset="utf-8" src="\/*.*.bundle\.js"><\/script>/, ''); // dynamic / import() bundles
+            html = html.replace(/<lit-route path="(.*)" component="(.*)" class="(.*)">(.*)<\/lit-route>/g, ''); // lit redux routes
+          } else if (optimization === 'spa') { // all the javascript, and async!
+            html = html.replace(/<script type="text\/javascript"/, '<script async="" type="text/javascript"');
+            html = html.replace(/<script charset="utf-8"/, '<script async="" charset="utf-8"'); // dynamic / import() bundles
+          }
   
           await fs.mkdirs(target, { recursive: true });
           await fs.writeFile(path.join(target, 'index.html'), html);
diff --git a/www/pages/about/how-it-works.md b/www/pages/about/how-it-works.md
index ea131af6..cd079232 100644
--- a/www/pages/about/how-it-works.md
+++ b/www/pages/about/how-it-works.md
@@ -20,6 +20,54 @@ This allows us to provide fine tuned workflows for both development and producti
 
 > Note: As powerful as **webpack** is, it does provide a lot of seemingly "magical" functionality out of the box, in particular its ability to use `import` to turn just about anything into a module (css, images, text files, etc).  While this is convenient at build time and for development, being able to use `import` non JavaScript assets is not part of any specification.  For this reason, we urge developers to understand what **webpack** does that _is_ spec compliant, and what it does that _isn't_.  Where possible, Greenwood will always favor a stable web platform first solution, e.g. if CSS Modules were to become a spec, Greenwood will make sure it is supported.
 
+### Optimization
+Greenwood supports a couple [different options](/docs/configuration#optimization) depending on what kind of site how you want to control the pre-rendering and bundling of your project.  Generally this comes down to if you need JavaScript to actually "run" your site.
+
+> _Ideally, Greenwood would like to offer a [**progressive** option](https://github.com/ProjectEvergreen/greenwood/issues/354) that only ships the JavaScript you actually need at runtime automatically._  All optimzation options aim to provide the best balance of performance and achieving as close to 💯for a Lighthouse score.
+
+#### Strict
+With this setting, _no bundled JavaScript_ is shipped at all.  For true static sites with projects that are "side effect" free (e.g. not using `fetch`, `setTimeout`, etc and just need to render some static HTML, will  yield the best results in regards to [performance scores](https://developers.google.com/web/tools/lighthouse).  What you write will only be used to pre-render your application.
+
+As a neat trick though, you can still use ["inline" JavaScript](https://github.com/ProjectEvergreen/greenwood/pull/401) that will get rendered inline in the HTML.
+
+```javascript
+import { html, LitElement } from 'lit-element';
+
+class HomeTemplate extends LitElement {
+
+  render() {
+    return html\`
+      <style>
+        /* some CSS if you need it */
+      </style>
+
+      <script>
+        function inlineScript() {
+          alert('inline script running even in SSG mode when loading no external JS! 🎉');
+        }
+      </script>
+      
+      <div class='gwd-content-wrapper'>
+        <button onclick="inlineScript()">For a good time, click here.</button>
+        <entry></entry>
+      </div>
+    \`;
+  }
+}
+
+customElements.define('page-template', HomeTemplate);
+```
+
+Caveats:
+- Code like [`fetch` and `setTimeout` in your JavaScript will not be available](https://github.com/ProjectEvergreen/greenwood/blob/v0.8.0/www/components/banner/banner.js#L37)
+- Your [CSS selectors cannot target custom elements](https://github.com/thegreenhouseio/www.thegreenhouse.io/pull/158/) (since `customElements.define` will not run)
+
+
+#### SPA (default)
+This option will bundle all your JavaScript, including a client side router provided by Greenwood (just like when development mode).  This will pre-render your site _and_ also ship a full "SPA" experience for your users and try and hydrate from that, but at the cost of shipping a little more JavaScript, so choose wisely!  
+
+> THere are a some [known issues](https://github.com/ProjectEvergreen/greenwood/labels/mode%3Aspa) we are tracking at this time as we refine support for this feature.
+
 ### Evergreen Build
 Greenwood promotes an "evergreen" build that ensures that the code delivered to users is as modern as the code all based on real browser usage and support statistics.  Automatically!
 
diff --git a/www/pages/docs/configuration.md b/www/pages/docs/configuration.md
index 820ed00b..97ea6ea4 100644
--- a/www/pages/docs/configuration.md
+++ b/www/pages/docs/configuration.md
@@ -98,6 +98,20 @@ Which would be equivalent to:
 <link rel="icon" href="/assets/favicon.ico">
 ```
 
+### Optimization
+Greenwood supports a couple different options for how it will generate a production build, depending on how much JavaScript you will need to serve your users.
+- **strict** (recommended for basic sites): What you write will only be used to pre-render your application. No JavaScript is shipped at all and will typically yield the best results in regards to performance.
+- **spa** (default): This will pre-render your site _and_ also ship a full "SPA" experience for your users.
+
+> _You can learn more about optimizations in our [How It Works](/about/how-it-works) docs._
+
+#### Example
+```js
+module.exports = {
+  optimization: 'spa'
+}
+```
+
 ### Public Path
 The `publicPath` options allows configuring additional URL segments to customize the [`<base href="/">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) for your site.
 
diff --git a/www/pages/docs/data.md b/www/pages/docs/data.md
index be9722f4..f34608ab 100644
--- a/www/pages/docs/data.md
+++ b/www/pages/docs/data.md
@@ -219,6 +219,7 @@ query {
       value,
       href
     },
+    optimization,
     publicPath,
     title,
     workspace


Test patch:
diff --git a/packages/cli/test/cases/build.config.error-mode/build.config.error-mode.spec.js b/packages/cli/test/cases/build.config.error-mode/build.config.error-mode.spec.js
new file mode 100644
index 00000000..3997d341
--- /dev/null
+++ b/packages/cli/test/cases/build.config.error-mode/build.config.error-mode.spec.js
@@ -0,0 +1,44 @@
+/*
+ * Use Case
+ * Run Greenwood build command with a bad value for optimization in a custom config.
+ *
+ * User Result
+ * Should throw an error.
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * {
+ *   optimization: 'lorumipsum'
+ * }
+ *
+ * User Workspace
+ * Greenwood default
+ */
+const expect = require('chai').expect;
+const TestBed = require('../../../../../test/test-bed');
+
+describe('Build Greenwood With: ', function() {
+  let setup;
+
+  before(async function() {
+    setup = new TestBed();
+    await setup.setupTestBed(__dirname);
+  });
+
+  describe('Custom Configuration with a bad value for Optimization', function() {
+    it('should throw an error that provided optimization is not valid', async function() {
+      try {
+        await setup.runGreenwoodCommand('build');
+      } catch (err) {
+        expect(err).to.contain('Error: provided optimization "loremipsum" is not supported.  Please use one of: strict, spa.');
+      }
+    });
+  });
+
+  after(function() {
+    setup.teardownTestBed();
+  });
+
+});
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.error-mode/greenwood.config.js b/packages/cli/test/cases/build.config.error-mode/greenwood.config.js
new file mode 100644
index 00000000..ee49cb18
--- /dev/null
+++ b/packages/cli/test/cases/build.config.error-mode/greenwood.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  optimization: 'loremipsum'
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js b/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js
index 7e844ba5..1777cfec 100644
--- a/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js
+++ b/packages/cli/test/cases/build.config.meta/build.config.meta.spec.js
@@ -83,7 +83,7 @@ describe('Build Greenwood With: ', function() {
       });
 
       it('should have one <script> tag in the <body> for the main bundle', function() {
-        const scriptTags = dom.window.document.querySelectorAll('body script');
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
         const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
           const src = script.src.replace('file:///', '');
 
@@ -93,6 +93,32 @@ describe('Build Greenwood With: ', function() {
         expect(bundledScript.length).to.be.equal(1);
       });
 
+      it('should have one <script> tag in the <body> for the main bundle loaded with async', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript[0].getAttribute('async')).to.be.equal('');
+      });
+
+      it('should have one <script> tag for Apollo state', function() {
+        const scriptTags = dom.window.document.querySelectorAll('script');
+        const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => {
+          return script.getAttribute('data-state') === 'apollo';
+        });
+
+        expect(bundleScripts.length).to.be.equal(1);
+      });
+
+      it('should have only one <script> tag in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
+
+        expect(scriptTags.length).to.be.equal(1);
+      });
+
       it('should have a router outlet tag in the <body>', function() {
         const outlet = dom.window.document.querySelectorAll('body eve-app');
 
diff --git a/packages/cli/test/cases/build.config.mode/build.config.mode.spec.js b/packages/cli/test/cases/build.config.mode/build.config.mode.spec.js
new file mode 100644
index 00000000..88085b2b
--- /dev/null
+++ b/packages/cli/test/cases/build.config.mode/build.config.mode.spec.js
@@ -0,0 +1,95 @@
+/*
+ * Use Case
+ * Run Greenwood with optimization setting in Greenwood config set to strict.
+ *
+ * User Result
+ * Should generate a bare bones Greenwood build with bundle JavaScript and routes.
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * {
+ *   optimization: 'spa'
+ * }
+ *
+ * User Workspace
+ * Greenwood default w/ nested page
+ *  src/
+ *   pages/
+ *     about/
+ *       index.md
+ *     hello.md
+ *     index.md
+ */
+const { JSDOM } = require('jsdom');
+const path = require('path');
+const expect = require('chai').expect;
+const runSmokeTest = require('../../../../../test/smoke-test');
+const TestBed = require('../../../../../test/test-bed');
+
+describe('Build Greenwood With: ', function() {
+  const LABEL = 'Custom Mode';
+  let setup;
+
+  before(async function() {
+    setup = new TestBed();
+    this.context = await setup.setupTestBed(__dirname);
+  });
+
+  describe(LABEL, function() {
+
+    before(async function() {
+      await setup.runGreenwoodCommand('build');
+    });
+
+    runSmokeTest(['public', 'not-found', 'hello'], LABEL);
+
+    describe('Strict', function() {
+      let dom;
+
+      beforeEach(async function() {
+        dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html'));
+      });
+      
+      it('should have no <script> tag in the <body>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+      
+        expect(scriptTags.length).to.be.equal(0);
+      });
+      
+      it('should have no <script> tags in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
+
+        expect(scriptTags.length).to.be.equal(0);
+      });
+
+      it('should have no <script> tags for Apollo state', function() {
+        const scriptTags = dom.window.document.querySelectorAll('script');
+        const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => {
+          return script.getAttribute('data-state') === 'apollo';
+        });
+      
+        expect(bundleScripts.length).to.be.equal(0);
+      });
+      
+      it('should have a router outlet tag in the <body>', function() {
+        const outlet = dom.window.document.querySelectorAll('body eve-app');
+      
+        expect(outlet.length).to.be.equal(1);
+      });
+      
+      it('should have only 2 route tags in the <body>', function() {
+        const routes = dom.window.document.querySelectorAll('body lit-route');
+      
+        expect(routes.length).to.be.equal(2);
+      });
+    });
+
+  });
+
+  after(function() {
+    setup.teardownTestBed();
+  });
+
+});
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.mode/greenwood.config.js b/packages/cli/test/cases/build.config.mode/greenwood.config.js
new file mode 100644
index 00000000..d7becfa7
--- /dev/null
+++ b/packages/cli/test/cases/build.config.mode/greenwood.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  optimization: 'strict'
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.mode/src/pages/about/index.md b/packages/cli/test/cases/build.config.mode/src/pages/about/index.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/cli/test/cases/build.config.mode/src/pages/hello.md b/packages/cli/test/cases/build.config.mode/src/pages/hello.md
new file mode 100644
index 00000000..fb8cf32d
--- /dev/null
+++ b/packages/cli/test/cases/build.config.mode/src/pages/hello.md
@@ -0,0 +1,7 @@
+---
+label: 'hello'
+title: 'Hello Page'
+---
+### Hello World
+
+This is an example page built by Greenwood.  Make your own in _src/pages_!
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.mode/src/pages/index.md b/packages/cli/test/cases/build.config.mode/src/pages/index.md
new file mode 100644
index 00000000..1c1a50fb
--- /dev/null
+++ b/packages/cli/test/cases/build.config.mode/src/pages/index.md
@@ -0,0 +1,3 @@
+### Greenwood
+
+This is the home page built by Greenwood. Make your own pages in src/pages/index.js!
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.title/build.config.title.spec.js b/packages/cli/test/cases/build.config.title/build.config.title.spec.js
index 90f28767..d5597d07 100644
--- a/packages/cli/test/cases/build.config.title/build.config.title.spec.js
+++ b/packages/cli/test/cases/build.config.title/build.config.title.spec.js
@@ -64,9 +64,8 @@ describe('Build Greenwood With: ', function() {
         expect(title).to.be.equal(configTitle);
       });
 
-      // rest of index smoke-test because <title></title> is changed for this case
       it('should have one <script> tag in the <body> for the main bundle', function() {
-        const scriptTags = dom.window.document.querySelectorAll('body script');
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
         const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
           const src = script.src.replace('file:///', '');
 
@@ -75,6 +74,32 @@ describe('Build Greenwood With: ', function() {
 
         expect(bundledScript.length).to.be.equal(1);
       });
+
+      it('should have one <script> tag in the <body> for the main bundle loaded with async', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript[0].getAttribute('async')).to.be.equal('');
+      });
+
+      it('should have one <script> tag for Apollo state', function() {
+        const scriptTags = dom.window.document.querySelectorAll('script');
+        const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => {
+          return script.getAttribute('data-state') === 'apollo';
+        });
+
+        expect(bundleScripts.length).to.be.equal(1);
+      });
+
+      it('should have only one <script> tag in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
+
+        expect(scriptTags.length).to.be.equal(1);
+      });
       
       it('should have a router outlet tag in the <body>', function() {
         const outlet = dom.window.document.querySelectorAll('body eve-app');
diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js b/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js
index d4cb63f9..e1d3571e 100644
--- a/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js
+++ b/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js
@@ -27,6 +27,8 @@ const { JSDOM } = require('jsdom');
 const path = require('path');
 const TestBed = require('../../../../../test/test-bed');
 
+const mainBundleScriptRegex = /index.*.bundle\.js/;
+
 describe('Build Greenwood With: ', function() {
   const LABEL = 'Data from GraphQL and using Custom Frontmatter Data';
   const apolloStateRegex = /window.__APOLLO_STATE__ = true/;
@@ -68,14 +70,44 @@ describe('Build Greenwood With: ', function() {
         });
       });
 
+      it('should have one <script> tag in the <body> for the main bundle', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript.length).to.be.equal(1);
+      });
+
+      it('should have one <script> tag in the <body> for the main bundle loaded with async', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript[0].getAttribute('async')).to.be.equal('');
+      });
+
       it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => {
-        const scriptTags = dom.window.document.querySelectorAll('head > script');
+        const scriptTags = dom.window.document.querySelectorAll('script');
         const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => {
           return script.getAttribute('data-state') === 'apollo';
         });
+        const innerHTML = apolloScriptTags[0].innerHTML;
+
+        expect(apolloScriptTags.length).to.equal(1);
+        expect(innerHTML).to.match(apolloStateRegex);
+      });
+
+      // two webpack bundles, and apollo state
+      it('should have only 3 <script> tag in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
 
-        expect(apolloScriptTags.length).to.be.equal(1);
-        expect(apolloScriptTags[0].innerHTML).to.match(apolloStateRegex);
+        expect(scriptTags.length).to.be.equal(3);
       });
 
       it('should have expected blog posts links in the <body> tag when using ChildrenQuery', function() {
diff --git a/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js b/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js
index f9801d9c..106ef502 100644
--- a/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js
+++ b/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js
@@ -34,6 +34,8 @@ const { JSDOM } = require('jsdom');
 const path = require('path');
 const TestBed = require('../../../../../test/test-bed');
 
+const mainBundleScriptRegex = /index.*.bundle\.js/;
+
 describe('Build Greenwood With: ', function() {
   const LABEL = 'Data from GraphQL';
   const apolloStateRegex = /window.__APOLLO_STATE__ = true/;
@@ -87,8 +89,30 @@ describe('Build Greenwood With: ', function() {
         });
       });
 
+      it('should have one <script> tag in the <body> for the main bundle', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript.length).to.be.equal(1);
+      });
+
+      it('should have one <script> tag in the <body> for the main bundle loaded with async', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript[0].getAttribute('async')).to.be.equal('');
+      });
+
       it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => {
-        const scriptTags = dom.window.document.querySelectorAll('head > script');
+        const scriptTags = dom.window.document.querySelectorAll('script');
         const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => {
           return script.getAttribute('data-state') === 'apollo';
         });
@@ -98,6 +122,12 @@ describe('Build Greenwood With: ', function() {
         expect(innerHTML).to.match(apolloStateRegex);
       });
 
+      it('should have only one <script> tag in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
+
+        expect(scriptTags.length).to.be.equal(1);
+      });
+
       it('should have a <header> tag in the <body>', function() {
         const header = dom.window.document.querySelectorAll('body header');
 
@@ -130,14 +160,32 @@ describe('Build Greenwood With: ', function() {
         expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'index.html'))).to.be.true;
       });
 
+      it('should have one <script> tag in the <body> for the main bundle', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript.length).to.be.equal(1);
+      });
+
       it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => {
-        const scriptTags = dom.window.document.querySelectorAll('head > script');
+        const scriptTags = dom.window.document.querySelectorAll('script');
         const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => {
           return script.getAttribute('data-state') === 'apollo';
         });
+        const innerHTML = apolloScriptTags[0].innerHTML;
+
+        expect(apolloScriptTags.length).to.equal(1);
+        expect(innerHTML).to.match(apolloStateRegex);
+      });
+
+      it('should have only one <script> tag in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
 
-        expect(apolloScriptTags.length).to.be.equal(1);
-        expect(apolloScriptTags[0].innerHTML).to.match(apolloStateRegex);
+        expect(scriptTags.length).to.be.equal(2);
       });
 
       it('should have a <header> tag in the <body>', function() {
@@ -196,13 +244,14 @@ describe('Build Greenwood With: ', function() {
       });
 
       it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => {
-        const scriptTags = dom.window.document.querySelectorAll('head > script');
+        const scriptTags = dom.window.document.querySelectorAll('script');
         const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => {
           return script.getAttribute('data-state') === 'apollo';
         });
+        const innerHTML = apolloScriptTags[0].innerHTML;
 
-        expect(apolloScriptTags.length).to.be.equal(1);
-        expect(apolloScriptTags[0].innerHTML).to.match(apolloStateRegex);
+        expect(apolloScriptTags.length).to.equal(1);
+        expect(innerHTML).to.match(apolloStateRegex);
       });
 
       it('should have a <header> tag in the <body>', function() {
diff --git a/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js b/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js
index 41c5c0f8..0050f2b4 100644
--- a/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js
+++ b/packages/cli/test/cases/build.default.workspace-template-app/build.default.workspace-template-app.spec.js
@@ -58,7 +58,7 @@ describe('Build Greenwood With: ', function() {
       });
 
       it('should have one <script> tag in the <body> for the main bundle', function() {
-        const scriptTags = dom.window.document.querySelectorAll('body script');
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
         const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
           const src = script.src.replace('file:///', '');
 
@@ -68,6 +68,32 @@ describe('Build Greenwood With: ', function() {
         expect(bundledScript.length).to.be.equal(1);
       });
 
+      it('should have one <script> tag in the <body> for the main bundle loaded with async', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
+        });
+
+        expect(bundledScript[0].getAttribute('async')).to.be.equal('');
+      });
+
+      it('should have one <script> tag for Apollo state', function() {
+        const scriptTags = dom.window.document.querySelectorAll('script');
+        const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => {
+          return script.getAttribute('data-state') === 'apollo';
+        });
+
+        expect(bundleScripts.length).to.be.equal(1);
+      });
+
+      it('should have only one <script> tag in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
+
+        expect(scriptTags.length).to.be.equal(1);
+      });
+
       it('should have a router outlet tag in the <body>', function() {
         const outlet = dom.window.document.querySelectorAll('body app-root');
 
diff --git a/packages/cli/test/unit/data/schema/config.spec.js b/packages/cli/test/unit/data/schema/config.spec.js
index 73897a66..b8959e94 100644
--- a/packages/cli/test/unit/data/schema/config.spec.js
+++ b/packages/cli/test/unit/data/schema/config.spec.js
@@ -53,6 +53,14 @@ describe('Unit Test: Data', function() {
         });
       });
 
+      describe('Mode', function() {
+
+        it('should have a default optimization setting of spa', function() {
+          expect(config.optimization).to.equal(MOCK_CONFIG.config.optimization);
+        });
+
+      });
+
       describe('Public Path', function() {
         const { publicPath } = MOCK_CONFIG.config;
 
diff --git a/test/smoke-test.js b/test/smoke-test.js
index b7ee6185..ed6862c7 100644
--- a/test/smoke-test.js
+++ b/test/smoke-test.js
@@ -56,6 +56,21 @@ function defaultNotFound(label) {
         expect(bundledScript.length).to.be.equal(1);
       });
 
+      it('should have no <script> tags for Apollo state', function() {
+        const scriptTags = dom.window.document.querySelectorAll('script');
+        const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => {
+          return script.getAttribute('data-state') === 'apollo';
+        });
+
+        expect(bundleScripts.length).to.be.equal(0);
+      });
+
+      it('should have no <script> tags in the <head>', function() {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
+
+        expect(scriptTags.length).to.be.equal(0);
+      });
+
       it('should have a <title> tag in the <head>', function() {
         const title = dom.window.document.querySelector('head title').textContent;
 
@@ -89,7 +104,7 @@ function defaultIndex(label) {
       });
 
       it('should have one <script> tag in the <body> for the main bundle', function() {
-        const scriptTags = dom.window.document.querySelectorAll('body app-root ~ script');
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
         const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
           const src = script.src.replace('file:///', '');
 
@@ -99,13 +114,15 @@ function defaultIndex(label) {
         expect(bundledScript.length).to.be.equal(1);
       });
 
-      it('should have one <script> tag for Apollo state', function() {
-        const scriptTags = dom.window.document.querySelectorAll('script');
-        const bundleScripts = Array.prototype.slice.call(scriptTags).filter(script => {
-          return script.getAttribute('data-state') === 'apollo';
+      it('should have one <script> tag in the <body> for the main bundle loaded with async', function() {
+        const scriptTags = dom.window.document.querySelectorAll('body > script');
+        const bundledScript = Array.prototype.slice.call(scriptTags).filter(script => {
+          const src = script.src.replace('file:///', '');
+
+          return mainBundleScriptRegex.test(src);
         });
 
-        expect(bundleScripts.length).to.be.equal(1);
+        expect(bundledScript[0].getAttribute('async')).to.be.equal('');
       });
 
       it('should have one <script> tag for Apollo state', function() {