Skip to content

Commit

Permalink
Merge pull request #936 from sharetribe/landingpage-performance-impro…
Browse files Browse the repository at this point in the history
…vements

Performance improvements
  • Loading branch information
Gnito authored Oct 1, 2018
2 parents 6eca916 + 47e8d18 commit ef62d2f
Show file tree
Hide file tree
Showing 14 changed files with 130 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ way to update this template, but currently, we follow a pattern:

## Upcoming version 2018-09-XX

* [change] Improve performance of public pages. Image assets are optimized and lazy loading is
applied to images in SectionLocation and ListingCard. Read
[documentation](./docs/improving-performance.md) for implementation details.
[#936](https://github.com/sharetribe/flex-template-web/pull/936)
* [change] Update sharetribe-scripts. **cssnext** (used previously in sharetribe-scripts) has been
deprecated. Now **postcss-preset-env** is used instead with stage 3 + custom media queries and
nesting-rules. If this change breaks your styling, you could still use v1.1.2. The next version of
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Documentation for specific topics can be found in the following files:
* [Original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md)
* [Customization checklist](customization-checklist.md)
* [Icons](icons.md)
* [Improving performance](improving-performance.md)

The application was bootstrapped with a forked version of
[create-react-app](https://github.com/facebookincubator/create-react-app). While most of the
Expand Down
74 changes: 74 additions & 0 deletions docs/improving-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Improving page rendering performance

When we think about page speed there are actually two different scenarios that we need to address:

* The speed of initial page load and possible reloads after that
* The speed of changing the page within Single page application (SPA)

The first one is usually a slower process. A browser needs to load all the HTML, CSS, JavaScript,
and images - and then it needs to understand and execute those files, calculate layout, paint
components and finally composite the whole view. The initial page load is the slowest since the
consequent page reloads can take benefit from browser caches.

SPAs can improve from that since they don't necessarily need to download anymore JavaScript, HTML,
or CSS - already downloaded JavaScript might be enough for rendering consequent pages when a user
wants to navigate to another page. Most of the time SPAs just fetch data for that page.

These two UX scenarios might also conflict with each other. If all the JavaScript is in one big
bundle, page changes within a SPA are fast. However, downloading and evaluating a big JavaScript
file is slowing initial page rendering down. Even though users rarely experience the full initial
page load speed when they use an SPA like Flex Template for Web, it is good to keep track of that
speed. Especially since that is what search engine bots are experiencing and therefore it might
affect your page rank.

Read more about
[website performance](https://developers.google.com/web/fundamentals/performance/why-performance-matters/).

We haven't yet implemented code splitting to reduce initial page rendering time, but there're other
improvements that could be done to improve both cases of page rendering.

## Check page performance

The first step is, of course, to start measuring performance.
[Lighthouse](https://developers.google.com/web/tools/lighthouse/) is a good tool to check rendering
performance. At least check those pages that are visible to unauthenticated users (e.g. landing
page, search page, listing page, about page and other static pages).

Lighthouse will give you some tips about how to improve performance and other aspects that website
developers should think about.

## Optimize image sizes

If your page is showing images, you should check that the image size is not bigger than what is
needed. So, adjusting image dimensions is the first step, but you should also think about image
quality, advanced rendering options and possibly serving those images from CDN instead of from
within your web app.

Quick checklist:

* Check that the actual dimensions of an image match with DOM element's dimensions.
* Lighthouse suggests that image compression level should be 85% or lower.
[Read more](https://developers.google.com/web/tools/lighthouse/audits/optimize-images)
* Good rule-of-thumb is that use JPEG for images and photos, where PNG is better for graphics, such
as logos, graphs and illustrations.
* If you are using JPEG images, think about saving them as progressive JPEGs.
[Read more](https://cloudinary.com/blog/progressive_jpegs_and_green_martians) +
[Photoshop guide](https://helpx.adobe.com/photoshop-elements/using/optimizing-images-jpeg-format.html)
* If you are using PNG images, consider running them through PNG optimizers to reduce file size.
Plenty of options available, one example is [TinyPNG.com](https://tinypng.com)
* Think about serving images and other static assets from some CDN.
[Read more.](https://www.smashingmagazine.com/2017/04/content-delivery-network-optimize-images/)

## Lazy load off-screen images and other components

Another way of dealing with images is to lazy load those images that are not visible inside an
initially rendered part of the screen. Lazy loading these off-screen images can be done with helper
function: `lazyLoadWithDimensions` (from `util/contextHelpers/`). Check `SectionLocations` component
for details.

## Use sparse fields

Another way to reduce the amount of data that is fetched from API is sparse fields. This is a
relatively new feature and Flex template app has not yet leveraged it fully, but it is created to
reduce unnecessary data and speed up rendering. You can read more from
[Flex API docs](https://flex-docs.sharetribe.com/#sparse-attributes).
Binary file modified src/assets/background-1440.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions src/components/ListingCard/ListingCard.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';
import React, { Component } from 'react';
import { string, func } from 'prop-types';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { NamedLink, ResponsiveImage } from '../../components';
import { lazyLoadWithDimensions } from '../../util/contextHelpers';
import { propTypes } from '../../util/types';
import { formatMoney } from '../../util/currency';
import { ensureListing, ensureUser } from '../../util/data';
import { richText } from '../../util/richText';
import { createSlug } from '../../util/urlHelpers';
import config from '../../config';
import { NamedLink, ResponsiveImage } from '../../components';

import css from './ListingCard.css';

Expand All @@ -33,6 +34,13 @@ const priceData = (price, intl) => {
return {};
};

class ListingImage extends Component {
render() {
return <ResponsiveImage {...this.props} />;
}
}
const LazyImage = lazyLoadWithDimensions(ListingImage, { loadAfterInitialRendering: 3000 });

export const ListingCardComponent = props => {
const { className, rootClassName, intl, listing, renderSizes, setActiveListing } = props;
const classes = classNames(rootClassName || css.root, className);
Expand All @@ -55,7 +63,7 @@ export const ListingCardComponent = props => {
onMouseLeave={() => setActiveListing(null)}
>
<div className={css.aspectWrapper}>
<ResponsiveImage
<LazyImage
rootClassName={css.rootForImage}
alt={title}
image={firstImage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,9 @@ exports[`ListingCard matches snapshot 1`] = `
onMouseLeave={[Function]}
>
<div>
<ResponsiveImage
<lazyLoadWithDimensions(ListingImage)
alt="listing1 title"
className={null}
image={null}
noImageMessage={null}
rootClassName={null}
sizes={null}
variants={
Array [
Expand Down
13 changes: 11 additions & 2 deletions src/components/SectionLocations/SectionLocations.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { lazyLoadWithDimensions } from '../../util/contextHelpers';

import { NamedLink } from '../../components';

Expand All @@ -11,13 +12,21 @@ import helsinkiImage from './images/location_helsinki.jpg';
import rovaniemiImage from './images/location_rovaniemi.jpg';
import rukaImage from './images/location_ruka.jpg';

class LocationImage extends Component {
render() {
const { alt, ...rest } = this.props;
return <img alt={alt} {...rest} />;
}
}
const LazyImage = lazyLoadWithDimensions(LocationImage);

const locationLink = (name, image, searchQuery) => {
const nameText = <span className={css.locationName}>{name}</span>;
return (
<NamedLink name="SearchPage" to={{ search: searchQuery }} className={css.location}>
<div className={css.imageWrapper}>
<div className={css.aspectWrapper}>
<img src={image} alt={name} className={css.locationImage} />
<LazyImage src={image} alt={name} className={css.locationImage} />
</div>
</div>
<div className={css.linkText}>
Expand Down
Binary file modified src/components/SectionLocations/images/location_helsinki.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/components/SectionLocations/images/location_rovaniemi.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/components/SectionLocations/images/location_ruka.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/containers/AboutPage/AboutPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '../../components';

import css from './AboutPage.css';
import image from './about-us-1440.jpg';
import image from './about-us-1056.jpg';

const AboutPage = () => {
const { siteTwitterHandle, siteFacebookPage } = config;
Expand Down
Binary file added src/containers/AboutPage/about-us-1056.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/containers/AboutPage/about-us-1440.jpg
Binary file not shown.
32 changes: 27 additions & 5 deletions src/util/contextHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ export const withViewport = Component => {
* the shape `{ width: 600, height: 400}`.
*
* @param {React.Component} Component to be wrapped by this HOC
* @param {Object} options pass in options like maxWidth and maxHeight.
* @param {Object} options pass in options like maxWidth and maxHeight. To load component after
* initial rendering has passed or after user has interacted with the window (e.g. scrolled),
* use`loadAfterInitialRendering: 1500` (value should be milliseconds).
*
* @return {Object} HOC component which knows its dimensions
*/
export const lazyLoadWithDimensions = (Component, options) => {
export const lazyLoadWithDimensions = (Component, options = {}) => {
// The resize event is flooded when the browser is resized. We'll
// use a small timeout to throttle changing the viewport since it
// will trigger rerendering.
Expand All @@ -96,6 +98,7 @@ export const lazyLoadWithDimensions = (Component, options) => {
super(props);
this.element = null;
this.defaultRenderTimeout = null;
this.afterRenderTimeout = null;

this.state = { width: 0, height: 0 };

Expand All @@ -112,6 +115,15 @@ export const lazyLoadWithDimensions = (Component, options) => {
this.defaultRenderTimeout = window.setTimeout(() => {
if (this.isElementNearViewport(0)) {
this.setDimensions();
} else {
const loadAfterInitialRendering = options.loadAfterInitialRendering;
if (typeof loadAfterInitialRendering === 'number') {
this.afterRenderTimeout = window.setTimeout(() => {
window.requestAnimationFrame(() => {
this.setDimensions();
});
}, loadAfterInitialRendering);
}
}
}, RENDER_WAIT_MS);
}
Expand All @@ -121,11 +133,18 @@ export const lazyLoadWithDimensions = (Component, options) => {
window.removeEventListener('resize', this.handleWindowResize);
window.removeEventListener('orientationchange', this.handleWindowResize);
window.clearTimeout(this.defaultRenderTimeout);

if (this.afterRenderTimeout) {
window.clearTimeout(this.afterRenderTimeout);
}
}

handleWindowResize() {
if (this.isElementNearViewport(NEAR_VIEWPORT_MARGIN)) {
this.setDimensions();
const shouldLoadToImproveScrolling = typeof options.loadAfterInitialRendering === 'number';
if (this.isElementNearViewport(NEAR_VIEWPORT_MARGIN) || shouldLoadToImproveScrolling) {
window.requestAnimationFrame(() => {
this.setDimensions();
});
}
}

Expand Down Expand Up @@ -159,7 +178,10 @@ export const lazyLoadWithDimensions = (Component, options) => {
const { maxWidth, maxHeight } = options;
const maxWidthMaybe = maxWidth ? { maxWidth } : {};
const maxHeightMaybe = maxHeight ? { maxHeight } : {};
const style = { width: '100%', height: '100%', ...maxWidthMaybe, ...maxHeightMaybe };
const style =
maxWidth || maxHeight
? { width: '100%', height: '100%', ...maxWidthMaybe, ...maxHeightMaybe }
: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 };

return (
<div
Expand Down

0 comments on commit ef62d2f

Please sign in to comment.