Native Web Apps

React, JavaScript and WebAssembly to Rewrite Native Apps

@FlorianRival

I ❤️ apps

Since a long time

 open-source, cross-platform game creator designed to be used by everyone

  • An editor for games
  • Visual programming
  • Output HTML5 games

The editor is getting old

  • Lots of cross-platforms issues with the UI toolkit, support for macOS and Linux is bad.
  • Iterating on the software is slow (C++ 😬)
  • Building UI is slow...
  • ...and limited to old UI components...
  • ...and UX would need some enhancements 😇
  • The entry barrier for new contributors is high (C++ 😬)

It's time to react!

Could web technologies help?

React is perfect for making complex UI

Well, really?...

Can we have

list and trees of hundreds of elements, 

dynamic panels,

2d/3d visualizations

, nested

dialogs,

color pickers, tabs, file trees, a "real app" feeling

drag'n'drop, search,

2d/3d visualizations

, nested

dialogs,

color pickers, tabs, file trees, a "real app" feeling

drag'n'drop, search,

, copy-

paste things

, handle key-

board shortcuts

, display

and edit code in a rich editor

show charts...

Can we make ambitious apps?

JavaScript is perfect for most applications

Well, really?...

what if we

already have a native codebase?

Or have advanced

simulations/computations,

or need consistent perfs?

GUI (windows, dialogs...)

Filesystem

React.js powered interface

Node.js/browser adapters

X-platform toolkit (wxWidgets)

GDevelop C++ codebase

GDevelop JS codebase

GDevelop C++ codebase

Core classes (game, scene, object, textures...)
Game exporter (JS "transpiler")
libGDevelop.js

Writing WebAssembly

Using Emscripten

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk && ./emsdk install latest && ./emsdk activate latest
./emcc tests/hello_world.c
node a.out.js

For large projects, Emscripten provides replacements that swap GCC or Clang by Emscripten in build system (automake, CMake...)

Expose existing classes

class ObjectsContainer {
  Object& InsertObject(const Object& object, size_t position) { ... };
  Object& GetObject(const gd::String& name) { ... };
}

class Layout : public ObjectsContainer {
public:
  void setName(const gd::String & name_) { name = name_; };
  const gd::String & getName() { return name; };

  // ...
}
interface Layout {
  void setName([Const] DOMString name);
  [Const, Ref] DOMString getName();

  // Inherited from ObjectsContainer
  [Ref] Object insertObject([Ref] gdObject object, unsigned long pos);
  [Ref] Object getObject([Const] DOMString name);
}

"WebIDL" bindings for Emscripten WebIDL binder:

Use Emscripten classes

Module().then(gd => {
  const layout = new gd.Layout();
  layout.setName("My game level");
  const object = new gd.Object();
  object.setName("My character");
  layout.insertObject(object, 0);

  console.log(layout.getName()); // "My game level"
  console.log(object.getObject("My character")); // Returns a gd.Object
});
  • Converted, including JS strings (to char*)

Primitive types?

If I pass an object?

  • std::iostream is binded to console.log
  • Objects references are converted to pointers (or references) by the webIDL generated glue code

Can I debug?

Things to know

const layout = new gd.Layout();
// ...
gd.destroy(layout); // Binded objects need manual destruction
  • Memory management requires care!
  • For React:
componentDidMount() { this._project = new gd.Project(); }
componentWillUnmount() { gd.destroy(this._project); }

or use an Effect Hook 🔥

console.log(layout); // { ptr: 6804120 }

Wasm memory

The content of gd.Layout

Things to know (2)

Output files can be large (~3mb for GDevelop), but gzip helps.

Things to know (3)

  • A complete test set is invaluable for ensuring that no errors are in the bindings.
abort(16). Build with -s ASSERTIONS=1 for more info.
  • Wrong type for a parameter
  • Parameter forgotten
  • Using a deleted object

What can go wrong?

GUI (windows, dialogs...)

Filesystem

React.js powered interface

Node.js/browser adapters

X-platform toolkit (wxWidgets)

GDevelop JS codebase

GDevelop C++ codebase

Core classes (game, scene, object, textures...)
Game exporter (JS "transpiler")
libGDevelop.js

Creating a complex UI?

👉 Find a component library

  • Extensive list of high quality components
  • Good theming support
  • Accessibility
  • Documentation quality

Large lists (with drag'n'drop)

👉 Virtualized lists

const SortableObjectRow = SortableElement(props => (
  <ObjectRow ... />
));

const ObjectsList = (props) => (
  <List
    rowRenderer={({ ... }) => {
      <SortableObjectRow />
    })
  />
);

const SortableObjectsList = 
  SortableContainer(ObjectsList);

<SortableObjectsList
  onSortEnd={({ oldIndex, newIndex }) => ...}
  ...
/>
const ObjectRow = () => <div>...</div>;
const ObjectsList = (props) => props.map(group => <ObjectRow ... />)

react-sortable-hoc

react-virtualized

Panels/resizable editors

react-mosaic

👉 Use a tiling window library

Large trees (with drag'n'drop)

👉 Virtualization again!

react-sortable-tree

Levels rendering or visualizations

👉 Forget the DOM

👉 Don't forget the DOM

<svg
  onPointerMove={...}
  onPointerUp={...}
  width={this.props.imageWidth}
  height={this.props.imageHeight}
>
  <polygon
    fill="rgba(255,0,0,0.2)"
    stroke="rgba(255,0,0,0.5)"
    fileRule="evenodd"
    points={vertices.map(vertex =>
      `${vertex.getX()},${vertex.getY()}`
    ).join(' ')}
  />
  {vertices.map((vertex, j) => (
    <circle
      onPointerDown={...}
      key={`vertex-${j}`}
      fill="rgba(255,0,0,0.75)"
      cx={vertex.getX()}
      cy={vertex.getY()}
      r={5}
    />
  ))}
</svg>

When things aren't fast enough

👉 Profile the rendering

Measure performance in production (React development build is way slower)

shouldComponentUpdate/memoization is often the answer

👉 Inspect calls to WebAssembly

  • Beware of overhead in binding code/wasm call when calling functions, in particular if rendering large lists
objects.filter(object => object.getType() !== "") // N wasm calls + string conversion
  .map(object => object.getVariables(project));   // N wasm calls + string conversion

Won't be noticeable in most UI situations, but can be if used in renderings or long lists/trees

  • It might be useful to store strings/values on the JS side if you know they won't change.

When your codebase is growing

👉 Stick to your components

<FlatButton style={{fontSize: 12}}>
  Help
</FlatButton>
<FlatButton>
  Help
</FlatButton>

😒

☺️

🤩

<HelpButton />

Avoid custom/adhoc styles, that won't scale:

Make a component for it! #DesignSystem

👉 Don't unit test your visual components...

Well actually, do, but only for part where there is logic

👉 ...but create visual stories

Storybook

React Styleguidist

👉 Use types

Will tell you that you (or someone else) forgot to pass/remove a prop

Flow

Typescript

Help auto-completion

Avoid most silly (or not so silly) mistakes

Help to "document" your objects

Types shine when you or someone else is refactoring some parts of the app

... all of these which becomes really useful when you're back on some module you (or someone else) wrote a few months ago

GUI (windows, dialogs...)

Filesystem

React.js powered interface

Node.js/browser adapters

X-platform toolkit (wxWidgets)

GDevelop JS codebase

GDevelop C++ codebase

Core classes (game, scene, object, textures...)
Game exporter (JS "transpiler")
libGDevelop.js

Consider your packaging options

GDevelop

Electron

Web app

Embed in a native app

Squoosh.app

Spotify

Steam Chat

👉 Build over Electron APIs

export default class ElectronMenuImplementation {
  buildFromTemplate(template) {
    this.menuTemplate = template;
    return undefined;
  }

  showMenu(dimensions) {
    if (!electron) return;

    const { Menu } = electron.remote;
    const browserWindow = electron.remote.getCurrentWindow();
    this.menu = Menu.buildFromTemplate(this.menuTemplate);
    this.menu.popup({
      window: browserWindow,
      x: Math.round(dimensions.left),
      y: Math.round(dimensions.top + dimensions.height),
      async: true, // Ensure the UI is not blocked on macOS.
    });
  }
}
class MaterialUIContextMenu extends React.Component { ... }
class ElectronContextMenu extends React.Component { ... }
export default class MaterialUIMenuImplementation {
  buildFromTemplate(template) {
    return template.map((item, id) => {
      if (item.type === 'separator') {
        return <Divider key={'separator' + id} />;
      } else if (item.type === 'checkbox') {
        return (
          <MenuItem
            ...
          />
      } else {
        return (
          <MenuItem
            ...
          />
      }
  }

  showMenu() { ... }
}

GUI (windows, dialogs...)

Filesystem

React.js powered interface

Node.js/browser adapters

X-platform toolkit (wxWidgets)

GDevelop JS codebase

GDevelop C++ codebase

Core classes (game, scene, object, textures...)
Game exporter (JS "transpiler")
libGDevelop.js

Result?

What are users saying?

Wow, what a difference a year can make 

but I have to admit, once you worked a bit with it, it could be really more productive on many aspects than GD4

Amazing how much easier and streamlined the engine has become

It works!

The native web app is better than the native app

Ultra fast iterations,

ultra

fast testing (storybook)

, near

perfect cross-platform

, faster

startup time

, auto updates,

more contributors,

"try it

online",

tablets/phones port

Thanks! ☺️

@FlorianRival

gdevelop-app.com

github.com/4ian/GDevelop

Ping me

Check GD

Native Web Apps: React and WebAssembly to Rewrite Native Apps

By Florian Rival

Native Web Apps: React and WebAssembly to Rewrite Native Apps

React, useful modules from the ecosystem and WebAssembly to create ambitious apps. Patterns and tips for create scalable applications at a fast pace.

  • 4,215