Building Emojityper 2️⃣👌🎁

with modern JS and ES6 Modules 🎆

In my spare time, I build Emojityper along with Alex: it’s a website where you type words to receive emoji. It loads fast and works offline. 📶

⛵🤦🏼💨 , and type Enter to copy

Go on, bookmark it now — the article will wait. Our aim is to be the most convenient emoji retrieval site for PCs (we support phones 📱, but it’s not our target 🎯). We support ‘modern browsers’ plus IE11—in 2017, IE11 is ~3% of global browser use—but we’re aiming at the low end where the % is higher.

This is an article about the technologies used in the web frontend, focusing on why and how we use JS and ES6 Modules. If you’d just like to learn more about emoji—I’m writing ✍️ an article for that too, coming real soon now™️.

0. Build it yourself

If you’d like to follow along at home 🏡, head over to our GitHub repo. You can check out the code and run it quickly (instructions for Mac/*nix)—

git clone https://github.com/emojityper/emojityper.git
yarn install # needs yarn installed, NPM might work too
python -m SimpleHTTPServer # or use serve or other HTTP servers

You can then load the local server in a browser that supports ES6 modules out of the box—Safari 10.1+, Chrome M61+, and Firefox/Edge with flags—no compile steps needed. 🏗🚫️

If you’d like to build the browsers, run gulp dist. You can then serve from the ./dist folder and open in any supported browser. This is the folder we serve on emojityper.com (and ➡️ you can also check out deploy.sh for how we release internally).

1. Modern Modulation

Emojityper uses ES6 Modules for its JavaScript. To recap, the simplest website 📄🕸️ using ES6 Modules looks a bit like this:

<!DOCTYPE html>
<html>
<head>
<script type="module" src="yourcode.js"></script>
</head>
<body>
<h1>Hello</h1>
</body>
</html>

Looks simple —but 🤔 why not just use a normal script tag? —well, as a module, yourcode.js can trivially import further files, like you can in any other mainstream language (e.g., Go, Python, Java…):

// yourcode.js
import {helperFunction} from './library.js';
window.addEventListener('load', (event) => {
helperFunction()
});

Why ❓ do we want this?

The JavaScript World 🌏 Without Modules

Before modules, you had ️two ✌ options to lay out complex web projects:

1️⃣ transpile multiple files using commonJS imports* (aka require() calls), having a build step every save; or
2️⃣ string together <script> tags inside HTML, to define their order.

Both have their downsides 😔. The latter is incredibly fragile, and the former adds development overhead—especially for otherwise simple sites—as no browser understand require() natively (although commonJS does make it easy to depend on 3rd-party 3️⃣🎉 code inside node_modules).

*There are other module systems, but commonJS is the most prevalent. 📊

…With Modules

With ES6 Modules, you can write code across different files—hit reload 🔃—and have an immediate engineering 🛠️ cycle. There’s no build or transpile step, and this works in development or production.

Emojityper works like this — its HTML loads the src/bundle.js module, which just contains imports to further modules required for the site to run. Conceptually, every file inside src manages some part or component of the site —e.g., input.js for the main input box ⌨️, and buttons.js for the “Copy” button 🔵 in the site’s top-right ↗️ corner.

Notably, ES6 Modules are implicitly run at the end of a page—as if they all had the defer ⏰ keyword set. You can’t turn this feature off.

The High Water Mark

Because only modern browsers support ES6 Modules, we can safely use modern 👨🏾‍🔬 JavaScript features during development—including language features like async, await and arrow functions—as well as 📡 modern parts of the standard library, like window.fetch and Promise.

If you’re confused by these features—we’ve all been there—so don’t worry. JavaScript is changing rapidly 🐣➡️🐥—but the win here is that browsers that support ES6 Modules also support these amazing new parts of JS, natively. When you read about how “chaining Promises is going to make your code 100x better”—you can quickly try it out in your real browser.

2. Archaic ES5 Output

Writing with ES6 Modules during development is great, but as mentioned, the earliest browser Emojityper supports is IE11 (earlier than that, and you’ll get an emojiless 😢 error page). So like many projects, we transpile our code to ES5 before it’s shipped—but our Gulp task rollup-nomodule looks like:

gulp.task('rollup-nomodule', function() {
const options = {
plugins: [ // all 'rollup-plugin-...'
commonJS(),
babel(),
uglify({output: {ascii_only: true}}), // ignore emoji
],
cache: false, // cache clobbers different rollup runs
};
return gulp.src(['src/support/*.js', 'src/bundle.js'])
.pipe(sourcemaps.init())
.pipe(rollup(options, {format: 'iife'}))
.pipe(concat('support.min.js'))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('./dist'));
});

This task basically calls Rollup—which ‘bundles’ 🗜️ our ES6 modules into a single file—to generate support.min.js, incorporating a few plugins: 🔌

  • commonJS: this allows us to import commonJS modules which normally need require()more on this later ⏱️
  • Babel: to transpile from ES6 to ES5 code—and note that Babel is largely configured in the .babelrc file for your project, and that’s out of scope for this blogpost 📝
  • Uglify: to shrink 📉 the output code size further.

Support Scripts

You might have noticed that the Gulp task depends on src/support/*.js — as well as src/bundle.js (our ES6 module 🚪 entry point). Let’s talk about the files inside support — these are files that are only run on browsers that don’t support ES6 modules natively.

There’s two scripts here: one to check for browser support, and the other that provides polyfills for browsers IE11+ and up. Inside polyfill.js, we actually import 😮 commonJS modules. This happens via the commonJS Rollup plugin (that I called out, above)—which isn’t part of ES6 Modules, but it doesn’t need to be, since this code is never run without being built first.

How do we decide what to polyfill? Well—it’s manually curated ✒️ based on the features we use that aren’t supported by our baseline of IE11+. Some examples include Array.from and Promise. We just import polyfills provided by the core-js library.

(Emojityper also uses Map, which is an ES6 feature, but has basic support in IE11—so it doesn’t need polyfilling. We actually use its presence to detect 🕵 supported browsers.)

No Module, No Cry

Finally, in the HTML 📜, we include support.min.js like this:

<script src="support.min.js" defer nomodule></script>

While you might be familiar with the defer keyword (it instructs your browser to only execute the script after the page is loaded), the nomodule keyword is definitely new. It instructs 📣 browsers that support ES6 Modules to completely ignore 🙅 this tag—they don’t need to load the support JS.

3. Singular ES6 Support

As we mentioned above—while we’re developing Emojityper, we use ES6 Modules across different files—to improve our engineering 🛠️ cycles. While it’s possible to do the same in production (for modern, supported browsers), we don’t, for 2️⃣ reasons:

  1. to prevent long ‘request chains’ ⛓
  2. and to save bytes. 🌮

What is a request chain, you ask? As a browser parses ES6 modules, it will 🔎 discover a tree of dependencies that it needs to fetch. It might look like:

borrowed from my previous post, ES6 Modules in Chrome Canary (M60+)

As index.html only knows about main.js, it will have to fetch it completely before fetching further files like depB.js and more.js. Each dependency adds another link in the chain—another round trip 🔃 to the server. 🏭

Push to the Rescue

You can solve this with HTTP/2 Server Push—basically, giving the browser the files they need before they ask 📥. Emojityper is cached via Cloudfare, which does support H2 Push, but its content is actually hosted on GitHub static pages—which can’t yet be configured to send the required headers. ❌

Roll Up, ES6

Instead of using the same approach in production, we use Rollup—like our ES5 story, and with the Uglify 👺 plugin—to generate a single, modern ES6 file and serve that via <script type=”module”>. This is counter-intuitive—why do we still ship ⛵ as a module? 😕

You might have noticed 🔔 that we’re missing a step 👣—we don’t transpile our code with Babel. This is intentional — in the vast majority of cases, features that we transpile to ES5—like the ones Emojityper uses, such as await or arrow functions—are much slower 🐢 in supported browsers than their native counterparts. There’s also a size cost in including ES5 support code (e.g., for generators).

Shake that Tree

The combination of Rollup and Uglify also brings our code size down via tree shaking. 🌴🍃 Basically, this statically removes unused methods or functions, maybe left over from development 👩‍💻—and this is only possible if we roll up all our code together first. 🥇

4. Build Buildables

Emojityper also depends on a few other build steps, which we’ll include for completeness. 💯

In development, we include the client-side Less script to compile our .less CSS file inside the browser. In production, we have a Gulp task that compiles the source to pure css that browsers understand natively. 👏

Demand deltas on our DOM nodes

In development, Emojityper uses a static index.html page. This is regular old HTML—it’s not annotated ✍️ with special markup or other code. It includes links to the ES6 Module code and the client-side Less script.

We don’t have a 2nd version of the file for production—instead, we actually have a Gulp task 🏗️ that performs standard DOM operations 🏥 to build the page. It is passed a document object, and looks a bit like this:

gulp.task('html', function() {
const mutator = (document, file) => {
// replace lessCSS with actual styles
document.getElementById('less').remove();
const link = Object.assign(document.createElement('link'),
{href: 'styles.css', rel: 'stylesheet'});
document.head.appendChild(link);
    // fix paths for module/nomodule code
const h = document.head;
h.querySelector('script[nomodule]').src = 'support.min.js';
h.querySelector('script[src^="src/"]').src = 'bundle.min.js';
};
return gulp.src('*.html')
.pipe(tweakdom(mutator))
.pipe(gulp.dest('./dist'));
});

This is available in a Gulp plugin called gulp-tweakdom. It was inspired by the approach used in Google’s Santa Tracker 🎅🏻. As frontend engineers, we believe it’s the most natural 😌 way to generate static HTML—rather than concatenating header 💆 and footer 👣 files together or, worse, inserting awkward non-HTML tags. 😩

Satisfying Source Maps

Regardless of how your browser runs Emojityper—whether it’s using ES5, or ES6—we generate source maps. These are debugging files stored alongside your JavaScript, with a .map extension. They allow your browser to map 🗺️ the compiled, transpiled versions of code 🔙 to their original source.

You don’t need this for development 🔨—errors happen at their actual location—but it allows debugging 🐞 in production. Importantly, even browsers without support for ES6 modules 🚫 will use source maps to correctly show errors in their original source files.

debugging in Firefox 54, without ES6 Module support

5. Emojityper as a PWA

Emojityper is also available as a Progressive Web App—it works totally offline and can be added to your (mobile) home screen. PWAs are a broad way to describe modern website that have native-like features—in our case, like working offline 📵, but also native Push Notifications! 🔔

We use Workbox 📦 to help generate a Service Worker—providing the offline part of Emojityper. A Service Worker is effectively an installable proxy that lives between the open versions of Emojityper and the internet 🌐. Workbox provides a way to specify what we’d like to make available offline—in this case, our HTML/CSS/JS—and to generate a manifest file describing what to be ‘installed’, or made offline, on Emojityper’s first load ⬇️.

Workbox can be used in a number of ways—in our case, we use it just to generate a manifest file, while writing our own Service Worker 👨‍💻. The Gulp task to generate a manifest file needs to run after all others—to correctly identify the files being included—and looks 👀 like:

gulp.task('manifest', ['css', 'js', 'html', 'static'], function() {
return workbox.generateFileManifest({
manifestDest: './dist/manifest.js',
globPatterns: ['**/*.{png,html,js,json,css}'],
globDirectory: './dist',
  // don't include sourcemaps or SW itself
globIgnores: ['*.map', 'sw.js', 'manifest.js'],
  // treat files as relative to SW
modifyUrlPrefix: {'/': './'},
});
});

Workbox also lets us specify runtime caching rules inside the Service Worker, directly—in our case, we specify that anything from the Google Fonts ✒️🔤 should be cached. We can’t describe the exact font files here 🔮—as different platforms support different formats.

As an aside, I’ll also note that many static resources (including Google Fonts), are cached by the browser too — due to long Cache-Control headers—but they still expire eventually, hence why we include them too. ⌛

Emoji database

The emoji database 📂 is actually cached in localStorage, and not part of the Service Worker—this saves a huge download every time the site is opened, which is important for browsers that don’t support Service Workers.

DIY 👷🏾👷🏼‍️

If you’ve read this far—that’s great! 🎊

But if you’re still unclear as to how to get started: keep in mind that the main build step used in Emojityper is to use Rollup 🗞️ to roll up your module into a single file. Even our invocations of say, Babel—to convert to ES5, for non-ES6 Module browsers—or Uglify—to shrink our output size—are done entirely through Rollup plugins. 🔌

Start by building your modern code with ES6 Modules. Add a Rollup task to your build process (or create a process using Gulp, Grunt or a trendy 📈 tool), and then include the Babel plugin to convert to ES5. From there, modify your output HTML to include the ES5 code with nomodule, and the ES6 code via <script type="module">.

Summary

Emojityper is a fun project to work on! It espouses modern JS development practices 🔬, while still targeting a common low baseline—and shipping a fast, single ES6 module to those users on up-to-date browsers. 🌟

There’s still work to be done—we can always get better at matching emoji. We’d also love to support HTTP/2 Server Push, which will reduce the initial load time ⏳ of Emojityper on supported browsers (including IE11+).

We hope you think it’s 😘👌 and that you emojoy it! 🥁💥😂

📥 Contact us at @mangopdf and @samthor.