Return to page

BLOG

5 Tips for Improving Your H2O Wave Apps

 headshot

By Martin Turoci | minute read | August 09, 2022

Blog decorative banner image

Let’s quickly uncover a few simple tips that are quick to implement and have a big impact.

Do not recreate navigation, update it

The most common error I see across the Wave apps is ugly navigation that seems to be laggy.

 

Laggy navigation.

 

The reason for this behavior is that we want to save the clicked value and set it explicitly. The explicit setting is not a problem on its own, the problem is if we recreate the card (assign q.page['key'] a new value).

 

from h2o_wave import main, app, Q, ui

 

 

@app('/')

async def serve(q: Q):

    # Check if a link is clicked or active in URL.

    if q.args['#'] is not None:

        # Set it to local state.

        q.client.current_nav = f'#{q.args["#"]}'

 

    # Create a navigation card.

    q.page['nav'] = ui.nav_card(

        box='1 1 2 -1',

        # Use saved value or go for a default.

        value=q.client.current_nav or '#menu/spam',

        title='H2O Wave',

        items=[

            ui.nav_group('Menu', items=[

                ui.nav_item(name='#menu/spam', label='Spam'),

                ui.nav_item(name='#menu/ham', label='Ham'),

                ui.nav_item(name='#menu/eggs', label='Eggs'),

                ui.nav_item(name='#menu/toast', label='Toast'),

            ])

        ]

    )

 

    await q.page.save()

Laggy navigation code.

Whereas the fix is straightforward — create the navigation once, and update its value afterward.

from h2o_wave import main, app, Q, ui

 

@app('/')

async def serve(q: Q):

    # Run this part only once, when the browser first connects.

    if not q.client.initialized:

        # Create a navigation card.

        q.page['nav'] = ui.nav_card(

            box='1 1 2 -1',

            # Use default value initially.

            value='#menu/spam',

            title='H2O Wave',

            items=[

                ui.nav_group('Menu', items=[

                    ui.nav_item(name='#menu/spam', label='Spam'),

                    ui.nav_item(name='#menu/ham', label='Ham'),

                    ui.nav_item(name='#menu/eggs', label='Eggs'),

                    ui.nav_item(name='#menu/toast', label='Toast'),

                ])

            ]

        )

        q.client.initialized = True

 

    # Check if a link is clicked or active in URL.

    if q.args['#'] is not None:

        # Update just the value.

        q.page['nav'].value = f'#{q.args["#"]}'

 

    await q.page.save()

Correct navigation code.

Giving us the nice, smooth, and fast result we wanted.

Nice smooth navigation.

 

Understand H2O Wave state scopes

One of the common coding errors when developing an app I see nowadays is using an incorrect type of state. This can cause both performance and UX problems. Please see the docs to get more info.

Typical performance problem — unnecessary file loading. Let’s say we have a scenario where we want to load a dataset file for people to download from the app later on. There is going to be only a single dataset for everyone. The typical code for this looks like this:

 

from h2o_wave import main, app, Q, ui

 

@app('/')

async def serve(q: Q):

    if not q.client.initialized:

        # Upload the file to the Wave server.

        download_path, = await q.site.upload(['dataset.csv'])

        q.client.initialized = True

 

    # TODO: Draw a UI to allow file downloads.

    await q.page.save()

 

Incorrect file upload.

 

Can you spot the problem here? The code above uploads the same file on every single browser connection (browser tab). That means that if 200 people visited our app, the very same file would be uploaded 200 times which is wasteful for both network bandwidth and disk storage. Moreover, this code makes every single user wait until the file is uploaded and only after then the UI is painted. The solution? Simply use q.app instead of q.client for these cases. Note that using q.app means that only the very first user needs to wait for the file to be uploaded.

However, sometimes you do not want to bother even the first app visitor. For these cases, one can use on_startup and on_shutdown hooks. These are just simple functions that run when your app gets started (prior to being available in a web browser) or is about to be turned off. The downside is that you do not have access to the q object since no UI is present at this stage. Can be useful for running some preparation scripts, training a model, cleanup logic, etc. For more info, see the docs.

Back to our state scopes discussion. We saw a potential performance problem. However, using an incorrect state can be problematic on the UX level as well. Let’s imagine we need to spawn a process that emits periodic updates to our app. If we put the process identifier into a q.client, the user can open a new tab and start another process which might be undesirable (he already has one going on for his account). Using q.user solves this problem painlessly as it ensures all browser tabs of a single user are synced properly.

You don’t always need to upload all your files

We have already seen the proper way to upload global files that are user-independent — upload them only once. However, the site.upload is more suited for dynamic cases when you do not know what files you want to provide your users with in advance. On the other hand, if you already have the dataset ready, there is a faster alternative.

The solution is to either use public-dir or private-dir depending on what kind of access you want to provide. The 2 are Wave server parameters that allow you to choose a directory that will serve files directly from the wave server. Since most of the time both the Wave app and Wave server is located on the same machine, they also have a common file system.

This also comes in handy when you use custom JS or CSS to tweak your app.

Give your machine a break

Has your computer ever turned into a helicopter due to its fan being too loud? Well, one of the reasons might be Wave reload feature during development time. If you look closely at the terminal output when running your app, you might notice subtle info about files being watched.

 

Wave reload mode file watcher.

 

By default, the python server that Wave uses watches your file system for changes and if it detects any, it simply reloads the whole app. This is very useful but can be troublesome if you have the main app file in a folder with other files that are not directly related to your app. In my case, that’s a virtual environment folder that tends to be quite heavy, adding a lot of extra work to the reloader process.

 

Actual project folder.

 

Instead, the solution is to create a separate src folder or some people prefer a folder named like their app, e.g. my-wave-app folder. Then move all the app-related files into it and execute wave run command from within the folder itself.

 

Use a dedicated folder for the app code.

 

 

Voila, we can enjoy our silent office again.

Do not write all the code yourself

The primary audience for H2O Wave are people working with Data Science and Machine Learning, thus not software engineers. For this reason, we want to do everything we can to make the whole app-building process as smooth and enjoyable as possible. So instead of writing your app from scratch, please go ahead and either download our VSCode extension or Jetbrains plugin, depending on your preferred IDE, which comes with plenty of predefined snippets like w_app_* that offer various starter app templates. Using them gives you a head start since you already begin with a working app that has generic stuff like navigation already figured out so the only thing left is to add your own visualizations and workflows.

Conclusion

Even though this blog post was not a deep dive like the previous ones, I believe even these little things can have a significant impact on your day-to-day Wave development. In case you feel like there is a topic that you would like to know more about, please do not hesitate and start a discussion with your suggestions!

 

 headshot

Martin Turoci