Why the mobile web sucks

Over at The Verge, Nilay Patel writes:

I hate browsing the web on my phone.

I do it all the time, of course — we all do. Just looking at the stats for The Verge, our mobile traffic is up 70 percent from last year, while desktop traffic is up only 11 percent. That trend isn’t going back; phones are just too convenient, beckoning us to waste more and more of our time gazing at their ever-larger screens.

But man, the web browsers on phones are terrible. They are an abomination of bad user experience, poor performance, and overall disdain for the open web that kicked off the modern tech revolution. […]

Now, I happen to work a media company, and I happen to run a website that can be bloated and slow. Some of this is our fault: The Verge is ultra-complicated, we have huge images, and we serve ads from our own direct sales and a variety of programmatic networks. Our video player is annoying. (I swear a better one is coming, for real this time.) We could do a lot of things to make our site load faster, and we’re doing them.

I couldn’t resist doing a quick analysis of page speed for that very article:

  • 22 seconds to fully load on desktop
  • 2.6MB of data
  • 81 separate JavaScripts
  • …plus 12 more JavaScripts blocked for XHR violations
  • …plus chartbeat.net trackers giving 503 errors (site over capacity)
  • …plus 3 Flash movies which would have bloated things up much further if I didn’t have Flash disabled

And yes, if I emulate a mobile device, it’s the same 2.6MB of data and 81 JavaScripts.

Gosh, I wonder why The Verge is miserable to read on mobile, eh?

Expecting mobile browser makers to magically solve your site’s performance problems is unrealistic. Mobile network data has high latency and (comparatively) low speed, and that’s inherent to the technology. You simply must engineer your web site with mobile browsers in mind.

For those who have ignored web performance, the current trend of more and more traffic coming from mobile browsers is going to lead to a painful reckoning.

XPages, Bootstrap, and form validation

I’ve recently been experimenting with Bootstrap for page layout, now that it’s an official part of the XPages Extension Library for Domino. After some simple test pages, I decided to put together a standard data entry form to see how painful that was.

A typical Bootstrap form’s HTML for a single field looks like this:

<div class="form-group">
  <label for="email1">Email address</label>
  <input type="email" class="form-control" id="email1" 
    placeholder="yourid@example.com">
  <span class="help-block">Your Internet e-mail address</span>
</div>

If you have a validation error to display, and want to show a feedback icon, the HTML balloons up to something like this:

<div class="form-group has-feedback has-error">
  <label for="email1">Email address</label>
  <input type="email" class="form-control" id="email1" 
    placeholder="yourid@example.com">
  <span class="glyphicon form-control-feedback glyphicon-delete"/>
  <span class="help-block with-errors">E-mail address is required.</span>
</div>

Nothing very difficult there, and a lot easier than OneUI, but by the time you convert it into XPages elements with rendered formulae and repeat it for a dozen form fields, you’re looking at a long and complicated page.

The obvious thing to want to do is encapsulate it into a custom control. I’m not the first person to do so, but I’ve never liked passing field names and data types to custom controls. Surely we could use editable facets for that?

The first piece of the puzzle was figuring out how to obtain a reference to the component dropped into a facet, from within the custom control. I eventually discovered that the facet contents are instantiated on page load, so you can look them up in the afterPageLoad event of your custom control.

It’s not quite that simple, though. getComponent with a facet’s ID returns a container object. The actual component inside the facet is a child object, accessed through the .children property.

Suppose your custom component has a callback (facet) with an id of field1, and suppose when you use the custom component you drop a text field on it. Inside the custom component, getComponent('field1').children[0] will be the text field object.

Once I get the instantiated component (i.e. form field) during the afterPageLoad event, I store its actual ID. That way, the rest of the code in the custom component can easily fetch it with getComponent(). But where to store it? Well, you could use all kinds of cunning methods, but XPages already hands you an object whose scope is the custom control — the compositeData object!

In other words, rather than passing the custom control a field name and other information needed for it to create a field via compositeData, I pass in an actual form control by dropping it on the component’s editable area, and the component stores the information it needs in compositeData for itself.

As well as storing the editable component’s ID, I also use the addAttr() method to add any HTML5 placeholder text that’s necessary, so the page using the component doesn’t have to deal with that mess either.

The next trick is detecting when there are any validation error messages. To do that, I use the formula facesContext.getMessages().hasNext(). The state of the dropped-on field in particular can be checked via getComponent(compositeData.fieldId)fieldId is where I stashed the control’s ID during the afterPageLoad event.

The final ingredient is how to validate and trigger the errors. I use the same method I’ve written up before, a single SSJS validation function in the querySave event.

My custom control has just three properties:

  • label, for the text to put above the field,
  • placeholder, for the optional placeholder text to put inside the field, and
  • help, for any optional additional help message to display under the field.

An example of using the custom control looks like this:

<xc:cc_formfield label="Bird name:" placeholder="Your bird's name">
  <xp:this.facets>
    <xp:inputText id="birdName" xp:key="field" />
  </xp:this.facets>
</xc:cc_formfield>

That’s it. Everything else is handled for you. You bind your inputText or other control to a document in the usual way. Same goes for specifying custom formatters, default values, typeahead, and all the other form field goodies.

Here’s the custom controller’s code. It looks a bit scary, but it makes the higher level form code much cleaner:

The final code is on Github along with a demo XPage.

Two OpenLDAP TLS gotchas

The scenario:

  • You’re using CentOS 7 or RHEL 7.
  • You’re using OpenLDAP.
  • You have TLS set up on OpenLDAP.
  • You are trying to perform a query against the server using ldapsearch.

Problem #1:

You get:

ldap_start_tls: Can't contact LDAP server (-1)
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)

Possible solution:

You’re using the -Z option (along with -h and -p) to specify the host and port and request TLS. That option doesn’t work.

Instead, just use the -H option and specify the parameters as an LDAPS URL.

For example:

ldapsearch -x -Z -h localhost -p 636 -b 'dc=example' '(objectClass=*)'

becomes:

ldapsearch -x -H ldaps://localhost:636/ -b 'dc=example' '(objectClass=*)'

This is actually mentioned on the OpenLDAP FAQ if you look hard enough:

Most clients now have a -Z flag which enables sending the StartTLS extended operation to the server. This extended operation initiates TLS negotiation. To use ldaps://, one must use -H ldaps://.

(Emphasis mine.) I’ve no idea what -Z is for, given that it isn’t needed if you use -H with an LDAPS URL, and doesn’t work if you don’t.


Problem 2:

Having worked out how to request a TLS connection, it still doesn’t work, so you try adding the -d 1 argument to turn on some debugging. In the client’s debug logs you see either:

TLS: error: tlsm_PR_Recv returned 0 - error 21:Is a directory

or

TLS: error: tlsm_PR_Recv returned 0 - error 13:Permission denied

Solution:

You probably have SELinux enabled. OpenLDAP doesn’t work with SELinux. Disable it and reboot the server, or work out how to persuade SELinux to let openldap open its own config files.