Developing and testing web software for multiple browsers is easier than you might think. But the chances are that you will need to change the way that you develop and test. Yes, browsers are different, and they can affect the final product of your code. But it doesn’t have to be this way. Over the years, I’ve developed a quality process that ensures that the web applications that I create work on all the standard browsers.
In this article, I intend to show you that process to help free you of the browser “limitation fallacy.” I will show you that you are limiting your browser compatibility unnecessarily. Using this newfound knowledge in your organization will help you be more effective. It will produce untold gains through your whole product lifecycle.
The Browsers
Did you know that there are at least 38 browsers in use as of this writing?
Most browsers are based on standard components that do not differ when interpreting HTML, CSS, and JavaScript. Developing for and testing on these components increases the quality and compatibility of your application. It frees your customer from the burden of only using the browsers that you’ve tested. It gives your customers the flexibility to use whatever browser they choose. Furthermore, developing to standards ensures that you maintain the highest quality in your application.
The point here is that, in web development, it is overwhelming to make your application work for every browser. Or better stated; it appears overwhelming at first glance. When you look closer, you will find that it is not that hard.
Jacquelyn Bulao published a very informative blog called “21+ Revealing Web Browser Market Share Statistics for 2020.” This blog gives you some fascinating browser statistics. I wish she had distinguished between the desktop and mobile segments because the statistics are even more “revealing.” Nevertheless, it is a great read.
In the browser world, there are HTML and JavaScript standards that are published and forever debated. Companies and open source projects then take up the challenge of interpreting these standards into something usable. As of this writing, the standards are HTML 5, CSS 3, and ECMAScript 6 (ES6). We typically break those down into “Rendering” and “JavaScript” engines even though they are arguably inseparable. When you look at the browsers with significant market share, we find that they use one of three engine kits:
- Blink/V8
- Gecko/SpiderMonkey
- Webkit/Nitro
Note: I do not include Trident/Chakra because Microsoft has discontinued the Edge Browser for the Chromium Edge browser.
Browser | Rendering Engine | JavaScript Engine |
Chrome | Blink | V8 |
Chromium Edge | Blink | V8 |
Opera | Blink | V8 |
Brave | Blink | V8 |
Firefox | Gecko | SpiderMonkey |
Safari | Webkit2 | Nitro |
Edge | Trident | Chakra |
It is important to note that the table changes when we look at the mobile world. In iOS, all the browsers are forced by Apple to use the Webkit2/Nitro engine kit. That is not the case in the Android platforms.
These engine kits take on the almost impossible task of implementing every part of the published standards. They do an outstanding job, but as you can see from the following table, they are not entirely there.
Kit | HTML5/CSS3 | ES6 |
Blink/V8 (Chrome, Edge, Opera, Brave) | 95% | 98% |
Gecko/SpiderMonkey (Firefox) | 91% | 98% |
Webkit2/Nitro (Safari/iOS) | 91% | 99% |
Development
91% of the HTML 5 standards have been implemented in all the rendering engines. All the JavaScript engines have implemented 98% of the ES6 standards. When you stay within those standards, you give your code a much better chance of working correctly in all browsers.
To further improve your chances, you need to understand how the browser architecture affects your code. Memory Management, Thread Management, and JavaScript processing is different in the three engine kits and the browser implementations. You need to understand the difference in the rendering tree and how that is affected by JavaScript. These slight differences will affect the way the customer perceives your application’s performance.
All the browsers implement a variation of the multi-process architecture. In general, the multi-process architecture ensures that the UI and IO threads remain responsive by offloading expensive operations to other processes.
In the Chromium-based browsers, every tab and plugin are held in their own separate process. This separation enables a very stable architecture by ensuring that the rendering processes don’t crash the main process. However, this comes at the cost of memory. As you open more tabs and install more plugins, the browser consumes more memory.
Firefox is more conservative about memory usage. The difference is evident in the way the tabs and add-ons are managed. Firefox limits the renderer processes to 4 and distributes the tabs and add-ons to these processes. This distribution pattern ensures that memory is consumed at a fractional rate compared to Chromium browsers. However, as you open more tabs and install more add-ons, the potential for crashes increases. If your application falls inside a process that contains a renderer that crashes, it will hit the client-side of your application.
Safari and Webkit2 also implement a multi-process architecture. But where Chromium has a renderer process for every tab, Webkit2 is split into multiple processes. Safari implements a Webkit UI process that houses the application logic. Then it spins up a Webkit Web process to do the actual rendering.
Among the three architectural variations, they all process JavaScript generally the same way; Inline <script> will pause the rendering. However, the optimization of the bytecode is different and can affect your code. All three JavaScript engines use an interpreter to generate bytecode. That bytecode runs your JavaScript. When a function is run often (is hot), that generated bytecode is picked up for optimization. The next step is where things change in every JavaScript engine.
V8 JavaScript engine uses one optimizing compiler with profiling data to generate highly optimized machine code.
SpiderMonkey uses two optimizing compilers. One produces a “baseline” optimization while the other creates a “speculative” optimization based on the profiling data. If the speculative optimization fails, it falls back to the baseline.
Nitro uses three optimizing compilers without profiling data. The output of the “baseline” optimizing compiler is sent to the “data flow” optimizing compiler. That output is then sent to the final (FTL) compiler.
Impact on Development
Now we’ve come to the first part of the process to ensure compatibility and high quality. Following these steps in development will create highly compatible quality code:
Stay within the boundaries of implemented standards.
When designing a web application, ensure that you stay within the 91% of HTML 5 standards that all browsers implement. Also, stay within the 98% of the ES6 standards.
Reuse functions in standard JavaScript files.
Give JavaScript the same intense optimization scrutiny that you give server code. Break it all down to reusable objects or functions so that the optimizing compiler has the right profiling data to create the highly optimized bytecode.
Do not load JavaScript inline.
Remember that doing this will cause the parser to pause while the JavaScript is parsed and bytecode is generated. Load your JavaScript at the bottom of the HTML file. Only load JavaScript at the top of the file if it does not modify the DOM in any way.
Use real-time web functionality over refreshes.
Sometimes you have to chase the UI based on what’s happening on the server. Don’t refresh via JavaScript! Use SignalR when you need to update anything client-side. Doing this ensures that highly-optimized bytecode is generated and reused from the cache.
Write code that checks the adherence to standards.
Test the browser capabilities on load. If you’re targeting the modern browsers, you should be targeting HTML5 and ES6. At crucial points/locations in your application, you should check to ensure that the browser has the capabilities. You will need to decide where these “crucial points” are based on all the factors that affect your application. In some applications, I have put this code on every page. However, most of the time, the best place is only on the login page.
Here is the code that I use.
function BrowserHasModernCapabilities() { try { // Test html5 document.createElement("canvas"); // Test ES6 Javascript class ಠ_ಠ extends Array { constructor(j = "a", ...c) { const q = (({ u: e }) => { return { [`s${c}`]: Symbol(j) } })({}) super(j, q, ...c) } } new Promise((f) => { const a = function* () { return "\u{20BB7}".match(/./u)[0].length === 2 || true } for (let vre of a()) { const [uw, as, he, re] = [new Set(), new WeakSet(), new Map(), new WeakMap()] break } f(new Proxy({}, { get: (han, h) => h in han ? han[h] : "42".repeat(0o10) })) }).then(bi => new ಠ_ಠ(bi.rd)); return true; } catch (e) { return false; } }
All credit should go to Netflix for the elegant approach.
Avoid OS-specific code
If you practice this, you will expand your capabilities to include other operating systems. If you write JavaScript that is calling the WinRT API, you limit yourself to Windows. Try to avoid this, and you will ensure that your application works on any client.
Testing
Testing is where you are going to gain the most return on your investment. If you stick to the browsers with noticeable market share (excluding the original Edge), you can narrow your testing to just three browsers. You can even narrow that down to two if you don’t include MacOS. I don’t recommend that, though. Testing on Chrome, Firefox, and Safari ensures that you are capturing the three Engine kits. Therefore, you can confidently say that your application will work on any HTML5 and ES6 compliant browser.
Notice that I did not encourage you to state the browsers that you support. On the contrary, I encourage you to avoid doing that. Instead, identify the standards that you support. Instead of saying that you support Chrome, say that you support HTML5. Your customers will then be free to use whatever standards-based browser they want to use. Even better, they are free to use whatever OS they chose.
Example:
“Gattaca Inc.” created a web application that is installed on a desktop or server but accessed by remote clients.
In your original scenario, the Gattaca software is tested on Chrome and Firefox because they don’t support macOS. In the installation document, they state that they only support Windows. However, they fail to say that it is only for installation. Since the application’s testing is not separated from the installation, they can only state that they support Chrome and Firefox on Windows.
In the proposed option, the Gattaca software is tested differently. The installation testing is separated from the client testing. The documentation is changed to say, “The installation is only supported on Windows.” The client (Browser) testing is performed via a remote client. These remote clients are Chrome, Firefox, and Safari. Your documentation can now say, “We support standards-based browsers.”
Impact on Testing
Now we’ve come to the second part of the process to ensure compatibility and high quality. Following these steps will make your testing more effective:
Test on Chrome, Firefox, and Safari.
Decouple your testing from the OS/Browser limitation. Test for compatibility with the three engine kits.
Profile memory usage.
Monitor memory usage on the client-side of your application. To be effective, you need to make sure that memory utilization stays optimal. This monitoring can be done with something as simple as the Task Manager in Windows or a better profiling tool.
Bibliography
Bulao, J. (2020, Oct 14). 21+ Revealing Web Browser Market Share Statistics for 2020. Retrieved from techjury: https://techjury.net/blog/web-browser-market-share/
kangax, webbedspace, & zloirock. (2020, Oct 14). ECMAScript 6 comparison. Retrieved from kangax github: https://kangax.github.io/compat-table/es6/
Kosaka, M. (2020, Oct 14). Inside look at a modern web browser. Retrieved from Google Developer Site: https://developers.google.com/web/updates/2018/09/inside-browser-part1
Mozilla. (2020, Oct 14). Firefox Browser Architecture. Retrieved from https://github.com/mozilla/firefox-browser-architecture
Mozilla. (2020, Oct 14). Multiprocess Firefox. Retrieved from Mozilla Developer Network: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Multiprocess_Firefox
Pollock, R. (2020, Oct 14). Mozilla Tech. Retrieved from The search for the Goldilocks browser and why Firefox might be “just right” for you: https://medium.com/mozilla-tech/the-search-for-the-goldilocks-browser-and-why-firefox-may-be-just-right-for-you-1f520506aa35
Reisn, C. (n.d.). Multi-process Architecture. Retrieved 10 14, 2020, from https://blog.chromium.org/2008/09/multi-process-architecture.html
Software Architecture – The Chromium Projects. (n.d.). Retrieved 10 14, 2020, from https://www.chromium.org/chromium-os/chromiumos-design-docs/software-architecture
Stewart, W. (n.d.). Web Browser History. Retrieved 10 14, 2020, from http://www.livinginternet.com/w/wi_browse.htm
Wikipedia. (2020, Oct 14). Acid3. Retrieved from Wikipedia: https://en.wikipedia.org/wiki/Acid3