├── .gitignore ├── README.md ├── assets └── guess-next-large.gif ├── components └── layout.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── about.js ├── example.js ├── index.js └── media.js ├── routes.json └── static ├── favicon.ico ├── guess.png └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | guess 3 | node_modules 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔮 Guess.js + Next.js 2 | 3 | **[Guess.js](https://github.com/guess-js/guess) is a collection of libraries & tools for enabling data-driven user-experience on the web.** 4 | 5 | In this particular example, we combine Guess.js with Next.js to introduce predictive prefetching of JavaScript bundles. Based on user navigation patterns collected from Google Analytics or other source, Guess.js builds a machine-learning model to predict and prefetch JavaScript that will be required in each subsequent page. 6 | 7 | Based on early benchmarks, this can improve the perceived page load performance with 20%. 8 | 9 | For more information on Guess.js, take a look at the following links: 10 | * [Google I/O announcement](https://www.youtube.com/watch?time_continue=2093&v=Mv-l3-tJgGk) by Addy Osmani 11 | * [Introducing Guess.js - a toolkit for enabling data-driven user-experiences on the Web](https://blog.mgechev.com/2018/05/09/introducing-guess-js-data-driven-user-experiences-web/) 12 | * [Using Guess.js with static sites](https://github.com/guess-js/guess/tree/master/experiments/guess-static-sites) 13 | * [Using Guess.js with Angular, React, and Gatsby](https://github.com/guess-js/guess/tree/master/packages/guess-webpack) 14 | 15 | ## Usage 16 | 17 | Here's how you can try the demo: 18 | 19 | ```bash 20 | git clone git@github.com:mgechev/guess-next 21 | cd guess-next && npm i 22 | npm run build && npm start 23 | ``` 24 | 25 | ![Demo](/assets/guess-next-large.gif) 26 | 27 | ## Integration 28 | 29 | Guess.js (**0.1.5 and above**) works with Next.js with only two points of integration. All you need to do is add the `GuessPlugin` to `next.config.js` and introduce a snippet for prefetching the pages which are likely to be visited next. 30 | 31 | The following sections describe both points in details. 32 | 33 | ### Webpack Config 34 | 35 | All you need is to extend the webpack config of your Next.js application is to add the `GuessPlugin` to `next.config.js` file, located in the root of your project. If the file does not exist, create it and add the following content: 36 | 37 | ```ts 38 | const { GuessPlugin } = require('guess-webpack'); 39 | 40 | module.exports = { 41 | webpack: function(config, { isServer }) { 42 | if (isServer) return config; 43 | config.plugins.push( 44 | new GuessPlugin({ 45 | GA: 'XXXXXX' 46 | }) 47 | ); 48 | return config; 49 | } 50 | }; 51 | ``` 52 | 53 | We set the value of the `webpack` property of the object literal we set as value to `module.exports`. We set it to a function which alters the `GuessPlugin` to the `config.plugins` array. Notice that we check if Next.js has invoked webpack on the server and we return. 54 | 55 | As a value of the `GA` property, we set a Google Analytics View ID. At build time, Guess.js will open a browser and try to get read-only access to extract a report and use it for the predictive analytics. 56 | 57 | *Note that Google Analytics is not the only provider you can use to provide the user navigation report that Guess.js uses. In this example application we provide the report from a JSON file.* 58 | 59 | ### Prefetch Pages 60 | 61 | The final piece of the integration is performing the actual prefetching. In your layout component (see `components/layout.js`) add: 62 | 63 | ```ts 64 | import { guess } from 'guess-webpack/api'; 65 | 66 | // ... 67 | 68 | if (typeof window !== 'undefined') { 69 | Object.keys(guess()).forEach(p => router.prefetch(p)); 70 | } 71 | 72 | // ... 73 | ``` 74 | 75 | Keep in mind that we check if `window` is `"undefined"`. This is required because we don't want to run Guess.js on the server. When we invoke `guess()`, we'll return a set of routes where each route will have an associated probability for the user to visit it. 76 | 77 | The routes that `guess()` returns depend on the Google Analytics report that it has extracted, together with the user's effective connection type. 78 | 79 | ## License 80 | 81 | MIT 82 | -------------------------------------------------------------------------------- /assets/guess-next-large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/guess-next/bb76489cd500f49b6ef45d8039653cd7075743ba/assets/guess-next-large.gif -------------------------------------------------------------------------------- /components/layout.js: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'next/router'; 2 | import { guess } from 'guess-webpack/api'; 3 | 4 | import Link from 'next/link'; 5 | import Head from 'next/head'; 6 | 7 | const layout = ({ router, children, title = '🔮 Next.js + Guess.js' }) => { 8 | let predictions = []; 9 | if (typeof window !== 'undefined') { 10 | predictions = Object.keys(guess()).sort((a, b) => a.length - b.length); 11 | predictions.forEach(p => router.prefetch(p)); 12 | } 13 | 14 | return ( 15 |
16 | 17 | {title} 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 39 |
40 |
41 | Navigate through the application to see the magic 42 |
43 | The user will likely visit ✨ 44 | 55 |
I used the statistics you already have to make this prediction.
56 |
57 |
{children}
58 |
59 | ); 60 | }; 61 | 62 | export default withRouter(layout); 63 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { GuessPlugin } = require('guess-webpack'); 2 | 3 | module.exports = { 4 | webpack: function(config, { isServer }) { 5 | if (isServer) return config; 6 | config.plugins.push( 7 | new GuessPlugin({ 8 | debug: true, 9 | reportProvider() { 10 | return Promise.resolve(JSON.parse(require('fs').readFileSync('./routes.json'))); 11 | }, 12 | runtime: { 13 | delegate: true 14 | } 15 | }) 16 | ); 17 | return config; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guess-next", 3 | "version": "0.0.0", 4 | "description": "Experiment for integration of Guess.js with Next.js", 5 | "scripts": { 6 | "dev": "next", 7 | "build": "next build", 8 | "start": "next start", 9 | "export": "npm run build && next export -o guess" 10 | }, 11 | "author": "Minko Gechev ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "guess-webpack": "^0.3.7", 15 | "next": "^9.3.2", 16 | "react": "^16.8.6", 17 | "react-dom": "^16.8.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pages/about.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/layout'; 2 | import Link from 'next/link'; 3 | 4 | export default () => ( 5 | 6 | Guess.js is a library for predictive prefetching 7 |
8 | for your applications. If you want to see Guess.js' logo 9 |
10 | visit the{' '} 11 | 12 | media 13 | {' '} 14 | page. 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /pages/example.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/layout'; 2 | 3 | export default () => ( 4 | 5 | Here's how you can configure Guess.js: 6 |
7 |
 8 |         
 9 |           new GuessPlugin
10 |           {'({'}
11 |           
12 | 13 | // GA view ID. 14 |
15 | GA 16 | : GAViewID 17 | , 18 |
19 | 20 | // Hints Guess to not perform pre-fetching and delegate this logic to 21 |
22 | // its consumer. 23 |
24 | runtime 25 | : {'{ '} 26 | delegate 27 | : true 28 | }, 29 |
30 | 31 | // Since Gatsby already has the required metadata for pre-fetching, 32 |
33 | // Guess does not have to collect the routes and the corresponding 34 |
35 | // bundle entry points. 36 |
37 | routeProvider 38 | : false 39 | , 40 |
41 | // Optional argument. It takes the data for the last year if not 42 | 43 |
44 | // specified. 45 |
46 | period 47 | : period ?{' '} 48 | period : undefined 49 | , 50 |
51 | }) 52 |
53 |
54 |
55 |
56 | ); 57 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../components/layout'; 3 | 4 | export default class Index extends React.Component { 5 | render() { 6 | return ( 7 | 8 |

Next.js + Guess.js 🔮

9 |

10 | This page demonstrates how you can use Guess.js for 11 |
predictive prefetching with Next.js 12 |

13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pages/media.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/layout'; 2 | 3 | export default () => ( 4 | 5 | This is the media page. Find the Guess.js logo here. 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "/example": 80, 4 | "/about": 20 5 | }, 6 | "/example": { 7 | "/": 20, 8 | "/media": 0, 9 | "/about": 80 10 | }, 11 | "/about": { 12 | "/": 20, 13 | "/media": 80 14 | }, 15 | "/media": { 16 | "/": 33, 17 | "/about": 33, 18 | "/example": 34 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/guess-next/bb76489cd500f49b6ef45d8039653cd7075743ba/static/favicon.ico -------------------------------------------------------------------------------- /static/guess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/guess-next/bb76489cd500f49b6ef45d8039653cd7075743ba/static/guess.png -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Roboto; 3 | } 4 | 5 | @keyframes appear { 6 | 0% { 7 | opacity: 0; 8 | transform: translate(20px); 9 | } 10 | 100% { 11 | opacity: 1; 12 | transform: translate(0px); 13 | } 14 | } 15 | 16 | .predictions { 17 | display: inline; 18 | padding: 0; 19 | } 20 | 21 | .predictions li { 22 | animation: appear 0.4s; 23 | display: inline-block; 24 | } 25 | 26 | .predictions li, 27 | .content { 28 | margin-right: 10px; 29 | font-weight: bold; 30 | } 31 | 32 | nav { 33 | text-transform: uppercase; 34 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 35 | 'Droid Sans', 'Helvetica Neue', sans-serif; 36 | text-decoration: none !important; 37 | font-size: 13px; 38 | } 39 | 40 | nav a { 41 | margin-right: 10px; 42 | } 43 | 44 | a:-webkit-any-link { 45 | text-decoration: none !important; 46 | color: #999; 47 | transition: color 0.2s; 48 | } 49 | 50 | a:-webkit-any-link:hover { 51 | color: #000; 52 | } 53 | 54 | .guess-logo { 55 | font-size: 2em; 56 | margin-right: 10px; 57 | transform: translateY(8px); 58 | display: inline-block; 59 | } 60 | 61 | .guess-logo img { 62 | width: 40px; 63 | } 64 | 65 | .explanation { 66 | margin-top: 10px; 67 | font-size: 12px; 68 | } 69 | 70 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 71 | 72 | code { 73 | background-color: #f9f9f9; 74 | font-size: 10px; 75 | /*font-weight: 100;*/ 76 | padding: 4px; 77 | border: none; 78 | border-radius: 4px; 79 | overflow: scroll; 80 | font-family: 'Fira Code', 'Monaco', Courier, monospace; 81 | } 82 | 83 | pre { 84 | font-size: 13px; 85 | word-wrap: break-word; 86 | -webkit-font-smoothing: subpixel-antialiased; 87 | -moz-osx-font-smoothing: auto; 88 | padding: 16px; 89 | padding-bottom: 14px; 90 | display: block; 91 | color: #808080; 92 | background-color: #f9f9f9; 93 | border: none; 94 | box-shadow: inset 0 0 0 1px #e4ebf2; 95 | border-radius: 4px; 96 | overflow: auto; 97 | line-height: 16px; 98 | } 99 | 100 | .highlight .hll { 101 | background-color: #d6d6d6; 102 | } 103 | .highlight { 104 | background: #ffffff; 105 | color: #4d4d4c; 106 | max-width: 350px; 107 | overflow-x: auto; 108 | } 109 | .highlight .c { 110 | color: #8e908c; 111 | } /* Comment */ 112 | .highlight .err { 113 | color: #c82829; 114 | } /* Error */ 115 | .highlight .k { 116 | color: #8959a8; 117 | } /* Keyword */ 118 | .highlight .l { 119 | color: #f5871f; 120 | } /* Literal */ 121 | .highlight .n { 122 | color: #4d4d4c; 123 | } /* Name */ 124 | .highlight .o { 125 | color: #3e999f; 126 | } /* Operator */ 127 | .highlight .p { 128 | color: #4d4d4c; 129 | } /* Punctuation */ 130 | .highlight .cm { 131 | color: #8e908c; 132 | } /* Comment.Multiline */ 133 | .highlight .cp { 134 | color: #8e908c; 135 | } /* Comment.Preproc */ 136 | .highlight .c1 { 137 | color: #8e908c; 138 | } /* Comment.Single */ 139 | .highlight .cs { 140 | color: #8e908c; 141 | } /* Comment.Special */ 142 | .highlight .gd { 143 | color: #c82829; 144 | } /* Generic.Deleted */ 145 | .highlight .ge { 146 | font-style: italic; 147 | } /* Generic.Emph */ 148 | .highlight .gh { 149 | color: #4d4d4c; 150 | font-weight: bold; 151 | } /* Generic.Heading */ 152 | .highlight .gi { 153 | color: #718c00; 154 | } /* Generic.Inserted */ 155 | .highlight .gp { 156 | color: #8e908c; 157 | font-weight: bold; 158 | } /* Generic.Prompt */ 159 | .highlight .gs { 160 | font-weight: bold; 161 | } /* Generic.Strong */ 162 | .highlight .gu { 163 | color: #3e999f; 164 | font-weight: bold; 165 | } /* Generic.Subheading */ 166 | .highlight .kc { 167 | color: #8959a8; 168 | } /* Keyword.Constant */ 169 | .highlight .kd { 170 | color: #8959a8; 171 | } /* Keyword.Declaration */ 172 | .highlight .kn { 173 | color: #3e999f; 174 | } /* Keyword.Namespace */ 175 | .highlight .kp { 176 | color: #8959a8; 177 | } /* Keyword.Pseudo */ 178 | .highlight .kr { 179 | color: #8959a8; 180 | } /* Keyword.Reserved */ 181 | .highlight .kt { 182 | color: #eab700; 183 | } /* Keyword.Type */ 184 | .highlight .ld { 185 | color: #718c00; 186 | } /* Literal.Date */ 187 | .highlight .m { 188 | color: #f5871f; 189 | } /* Literal.Number */ 190 | .highlight .s { 191 | color: #718c00; 192 | } /* Literal.String */ 193 | .highlight .na { 194 | color: #4271ae; 195 | } /* Name.Attribute */ 196 | .highlight .nb { 197 | color: #4d4d4c; 198 | } /* Name.Builtin */ 199 | .highlight .nc { 200 | color: #eab700; 201 | } /* Name.Class */ 202 | .highlight .no { 203 | color: #c82829; 204 | } /* Name.Constant */ 205 | .highlight .nd { 206 | color: #3e999f; 207 | } /* Name.Decorator */ 208 | .highlight .ni { 209 | color: #4d4d4c; 210 | } /* Name.Entity */ 211 | .highlight .ne { 212 | color: #c82829; 213 | } /* Name.Exception */ 214 | .highlight .nf { 215 | color: #4271ae; 216 | } /* Name.Function */ 217 | .highlight .nl { 218 | color: #4d4d4c; 219 | } /* Name.Label */ 220 | .highlight .nn { 221 | color: #eab700; 222 | } /* Name.Namespace */ 223 | .highlight .nx { 224 | color: #4271ae; 225 | } /* Name.Other */ 226 | .highlight .py { 227 | color: #4d4d4c; 228 | } /* Name.Property */ 229 | .highlight .nt { 230 | color: #3e999f; 231 | } /* Name.Tag */ 232 | .highlight .nv { 233 | color: #c82829; 234 | } /* Name.Variable */ 235 | .highlight .ow { 236 | color: #3e999f; 237 | } /* Operator.Word */ 238 | .highlight .w { 239 | color: #4d4d4c; 240 | } /* Text.Whitespace */ 241 | .highlight .mf { 242 | color: #f5871f; 243 | } /* Literal.Number.Float */ 244 | .highlight .mh { 245 | color: #f5871f; 246 | } /* Literal.Number.Hex */ 247 | .highlight .mi { 248 | color: #f5871f; 249 | } /* Literal.Number.Integer */ 250 | .highlight .mo { 251 | color: #f5871f; 252 | } /* Literal.Number.Oct */ 253 | .highlight .sb { 254 | color: #718c00; 255 | } /* Literal.String.Backtick */ 256 | .highlight .sc { 257 | color: #4d4d4c; 258 | } /* Literal.String.Char */ 259 | .highlight .sd { 260 | color: #8e908c; 261 | } /* Literal.String.Doc */ 262 | .highlight .s2 { 263 | color: #718c00; 264 | } /* Literal.String.Double */ 265 | .highlight .se { 266 | color: #f5871f; 267 | } /* Literal.String.Escape */ 268 | .highlight .sh { 269 | color: #718c00; 270 | } /* Literal.String.Heredoc */ 271 | .highlight .si { 272 | color: #f5871f; 273 | } /* Literal.String.Interpol */ 274 | .highlight .sx { 275 | color: #718c00; 276 | } /* Literal.String.Other */ 277 | .highlight .sr { 278 | color: #718c00; 279 | } /* Literal.String.Regex */ 280 | .highlight .s1 { 281 | color: #718c00; 282 | } /* Literal.String.Single */ 283 | .highlight .ss { 284 | color: #718c00; 285 | } /* Literal.String.Symbol */ 286 | .highlight .bp { 287 | color: #4d4d4c; 288 | } /* Name.Builtin.Pseudo */ 289 | .highlight .vc { 290 | color: #c82829; 291 | } /* Name.Variable.Class */ 292 | .highlight .vg { 293 | color: #c82829; 294 | } /* Name.Variable.Global */ 295 | .highlight .vi { 296 | color: #c82829; 297 | } /* Name.Variable.Instance */ 298 | .highlight .il { 299 | color: #f5871f; 300 | } /* Literal.Number.Integer.Long */ 301 | --------------------------------------------------------------------------------