August 9th, 2022

5 Tips for Improving Your Wave Apps

RSS icon RSS Category: Wave

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!

About the Author

Martin Turoci

Software engineer that hates repetitive tasks, praises automation, clean code and "right tool for the job" rule. Also a big javascript ecosystem fan.
In my free time, I teach ballroom dancing at both competitive and social level (as a former ballroom dancer).

Leave a Reply

+
Recap of H2O World India 2023: Advancements in AI and Insights from Industry Leaders

On April 19th, the H2O World  made its debut in India, marking yet another milestone

May 29, 2023 - by Parul Pandey
+
Enhancing H2O Model Validation App with h2oGPT Integration

As machine learning practitioners, we’re always on the lookout for innovative ways to streamline and

May 17, 2023 - by Parul Pandey
+
Building a Manufacturing Product Defect Classification Model and Application using H2O Hydrogen Torch, H2O MLOps, and H2O Wave

Primary Authors: Nishaanthini Gnanavel and Genevieve Richards Effective product quality control is of utmost importance in

May 15, 2023 - by Shivam Bansal
AI for Good hackathon
+
Insights from AI for Good Hackathon: Using Machine Learning to Tackle Pollution

At H2O.ai, we believe technology can be a force for good, and we're committed to

May 10, 2023 - by Parul Pandey and Shivam Bansal
H2O democratizing LLMs
+
Democratization of LLMs

Every organization needs to own its GPT as simply as we need to own our

May 8, 2023 - by Sri Ambati
h2oGPT blog header
+
Building the World’s Best Open-Source Large Language Model: H2O.ai’s Journey

At H2O.ai, we pride ourselves on developing world-class Machine Learning, Deep Learning, and AI platforms.

May 3, 2023 - by Arno Candel

Request a Demo

Explore how to Make, Operate and Innovate with the H2O AI Cloud today

Learn More