Bug 1308441 - Use react-virtualized for RequestList r?honza draft
authorRicky Chien <ricky060709@gmail.com>
Mon, 06 Mar 2017 10:07:14 +0800
changeset 496388 234be95ad6a63d2601a6dc775e3e0524a310636a
parent 496387 63b32d9c8c36649b524ec22f0736c8a4f3c9885f
child 496389 9618d9a1c3191cfd8c81c947de27820fa94f5981
push id48583
push userbmo:rchien@mozilla.com
push dateFri, 10 Mar 2017 03:16:43 +0000
reviewershonza
bugs1308441
milestone55.0a1
Bug 1308441 - Use react-virtualized for RequestList r?honza MozReview-Commit-ID: HSzNJ5FJbx8
devtools/client/netmonitor/components/moz.build
devtools/client/netmonitor/components/request-list-column-cause.js
devtools/client/netmonitor/components/request-list-column-domain.js
devtools/client/netmonitor/components/request-list-column-file.js
devtools/client/netmonitor/components/request-list-column-header.js
devtools/client/netmonitor/components/request-list-column-method.js
devtools/client/netmonitor/components/request-list-column-size.js
devtools/client/netmonitor/components/request-list-column-status.js
devtools/client/netmonitor/components/request-list-column-transferred.js
devtools/client/netmonitor/components/request-list-column-type.js
devtools/client/netmonitor/components/request-list-column-waterfall.js
devtools/client/netmonitor/components/request-list-content.js
devtools/client/netmonitor/components/request-list-empty.js
devtools/client/netmonitor/components/request-list-header.js
devtools/client/netmonitor/components/request-list-item.js
devtools/client/netmonitor/components/request-list-row.js
devtools/client/netmonitor/components/request-list.js
devtools/client/netmonitor/reducers/ui.js
devtools/client/netmonitor/request-list-tooltip.js
devtools/client/netmonitor/utils/request-utils.js
devtools/client/themes/netmonitor.css
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -1,15 +1,23 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'monitor-panel.js',
     'network-monitor.js',
-    'request-list-content.js',
+    'request-list-column-cause.js',
+    'request-list-column-domain.js',
+    'request-list-column-file.js',
+    'request-list-column-header.js',
+    'request-list-column-method.js',
+    'request-list-column-size.js',
+    'request-list-column-status.js',
+    'request-list-column-transferred.js',
+    'request-list-column-type.js',
+    'request-list-column-waterfall.js',
     'request-list-empty.js',
-    'request-list-header.js',
-    'request-list-item.js',
+    'request-list-row.js',
     'request-list.js',
     'statistics-panel.js',
     'toolbar.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-cause.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = ["cause"];
+
+/**
+ * Request list cause column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnCause() {
+  let name = "cause";
+  return (
+    Column({
+      cellRenderer: CauseColumnCell,
+      className: "requests-list-subitem requests-list-cause",
+      dataKey: name,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 100,
+      maxWidth: 100,
+      minWidth: 45,
+    })
+  );
+}
+
+RequestListColumnCause.displayName = "RequestListColumnCause";
+
+const CauseColumnCell = createFactory(createClass({
+  displayName: "CauseColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData } = this.props;
+    let { cause } = rowData;
+    let causeType = "";
+    let causeUri;
+    let causeHasStack = false;
+
+    if (cause) {
+      // Legacy server might send a numeric value. Display it as "unknown"
+      causeType = typeof cause.type === "string" ? cause.type : "unknown";
+      causeUri = cause.loadingDocumentUri;
+      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
+    }
+
+    return (
+      div({ title: causeUri },
+        div({
+          className: "requests-list-cause-stack",
+          hidden: !causeHasStack,
+        }, "JS"),
+        div({ className: "subitem-label" }, causeType),
+      )
+    );
+  }
+}));
+
+module.exports = RequestListColumnCause;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-domain.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+  "remoteAddress",
+  "securityState",
+  "urlDetails",
+];
+
+/**
+ * Request list domain column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnDomain({
+  selectDetailsPanelTab,
+}) {
+  let name = "domain";
+  return (
+    Column({
+      cellRenderer: (props) =>
+        DomainColumnCell(Object.assign(props, { selectDetailsPanelTab })),
+      className: "requests-list-subitem requests-list-domain",
+      dataKey: name,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 150,
+      maxWidth: 220,
+      minWidth: 60,
+    })
+  );
+}
+
+RequestListColumnDomain.displayName = "RequestListColumnDomain";
+
+const DomainColumnCell = createFactory(createClass({
+  displayName: "DomainColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+    selectDetailsPanelTab: PropTypes.func.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData, selectDetailsPanelTab } = this.props;
+    let {
+      urlDetails,
+      remoteAddress,
+      securityState,
+    } = rowData;
+
+    let iconClassList = ["requests-list-domain-icon"];
+    let iconTitle;
+    if (urlDetails.isLocal) {
+      iconClassList.push("security-state-local");
+      iconTitle = L10N.getStr("netmonitor.security.state.secure");
+    } else if (securityState) {
+      iconClassList.push(`security-state-${securityState}`);
+      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
+    }
+
+    let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
+
+    return (
+      div({},
+        div({
+          className: iconClassList.join(" "),
+          title: iconTitle,
+          onClick: () => {
+            if (securityState && securityState !== "insecure") {
+              selectDetailsPanelTab("security");
+            }
+          },
+        }),
+        div({ className: "requests-list-domain-url", title }, urlDetails.host),
+      )
+    );
+  }
+}));
+
+module.exports = RequestListColumnDomain;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-file.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+  "responseContentDataUri",
+  "urlDetails",
+];
+
+/**
+ * Request list file column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnFile() {
+  let name = "file";
+  return (
+    Column({
+      cellRenderer: FileColumnCell,
+      className: "requests-list-subitem requests-list-file",
+      dataKey: name,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 300,
+      maxWidth: 350,
+      minWidth: 80,
+    })
+  );
+}
+
+RequestListColumnFile.displayName = "RequestListColumnFile";
+
+const FileColumnCell = createFactory(createClass({
+  displayName: "FileColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData } = this.props;
+    let { urlDetails } = rowData;
+
+    return (
+      div({
+        className: "requests-list-url subitem-label",
+        title: urlDetails.unicodeUrl,
+      },
+        urlDetails.baseNameWithQuery,
+      )
+    );
+  }
+}));
+
+module.exports = RequestListColumnFile;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-header.js
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const SortDirection = require("devtools/client/shared/vendor/react-virtualized").SortDirection;
+const { getFormattedTime } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const WaterfallBackground = require("../waterfall-background");
+
+const { div } = DOM;
+
+/**
+ * Request list column header component
+ * Describes the header of a table column
+ */
+const RequestListColumnHeader = createClass({
+  displayName: "RequestListColumnHeader",
+
+  propTypes: {
+    columnData: PropTypes.object,
+    dataKey: PropTypes.string.isRequired,
+    label: PropTypes.string,
+    sortBy: PropTypes.string,
+    sortDirection: PropTypes.string,
+  },
+
+  componentDidMount() {
+    if (this.props.dataKey === "waterfall") {
+      // Create the object that takes care of drawing the waterfall canvas background
+      this.background = new WaterfallBackground();
+      this.background.draw(this.props);
+      // Measure its width and update the 'waterfallWidth' property in the store.
+      setTimeout(() => {
+        let { width } = this.refs.header.getBoundingClientRect();
+        this.props.columnData.resizeWaterfall(width);
+      }, 50);
+    }
+  },
+
+  componentDidUpdate() {
+    if (this.props.dataKey === "waterfall") {
+      this.background.draw(this.props);
+    }
+  },
+
+  componentWillUnmount() {
+    if (this.props.dataKey === "waterfall") {
+      this.background.destroy();
+      this.background = null;
+    }
+  },
+
+  renderWaterfallLabel(waterfallWidth, scale, label) {
+    let labels = [];
+    if (waterfallWidth && scale) {
+      // Set millisecond tick labels by 5ms
+      let timingStep = 5;
+      let scaledStep = scale * timingStep;
+
+      // Ignore any divisions < 60px that would end up being too close to each other.
+      while (scaledStep < 60) {
+        scaledStep *= 2;
+      }
+
+      // Insert one label for each division on the current scale.
+      for (let x = 0; x < waterfallWidth; x += scaledStep) {
+        let millisecondTime = x / scale;
+        let divisionScale = "millisecond";
+
+        // If the division is greater than 1 minute.
+        if (millisecondTime > 60000) {
+          divisionScale = "minute";
+        } else if (millisecondTime > 1000) {
+          // If the division is greater than 1 second.
+          divisionScale = "second";
+        }
+
+        let width = (x + scaledStep | 0) - (x | 0);
+        // Adjust the first marker for the borders
+        if (x === 0) {
+          width -= 2;
+        }
+        // Last marker doesn't need a width specified at all
+        if (x + scaledStep >= waterfallWidth) {
+          width = undefined;
+        }
+
+        labels.push(
+          div({
+            key: labels.length,
+            className: "requests-list-timings-division",
+            "data-division-scale": divisionScale,
+            style: { width },
+          }, getFormattedTime(millisecondTime))
+        );
+      }
+    }
+
+    return labels.length > 0 ? labels : "";
+  },
+
+  render() {
+    let {
+      columnData = {},
+      dataKey,
+      label,
+      sortBy,
+      sortDirection,
+    } = this.props;
+    let title;
+    let className = ["requests-list-header-button", `requests-list-${dataKey}`];
+
+    if (sortBy === dataKey) {
+      className.push(sortDirection === SortDirection.ASC ? "ascending" : "descending");
+      title = L10N.getStr(sortDirection === SortDirection.ASC ?
+        "networkMenu.sortedAsc" : "networkMenu.sortedDesc");
+    }
+
+    let { waterfallWidth, scale } = columnData;
+
+    return (
+      div({
+        ref: "header",
+        className: className.join(" "),
+        title,
+      },
+        dataKey === "waterfall"
+          ? this.renderWaterfallLabel(waterfallWidth, scale, label)
+          : div({ className: "button-text" }, L10N.getStr(`netmonitor.toolbar.${label}`)),
+        div({ className: "button-icon" }),
+      )
+    );
+  }
+});
+
+module.exports = RequestListColumnHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-method.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = ["method"];
+
+/**
+ * Request list method column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnMethod() {
+  let name = "method";
+  return (
+    Column({
+      cellRenderer: MethodColumnCell,
+      className: "requests-list-subitem requests-list-method",
+      dataKey: name,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 75,
+      maxWidth: 84,
+      minWidth: 55,
+    })
+  );
+}
+
+RequestListColumnMethod.displayName = "RequestListColumnMethod";
+
+const MethodColumnCell = createFactory(createClass({
+  displayName: "MethodColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData } = this.props;
+
+    return (
+      div({ className: "subitem-label requests-list-method" }, rowData.method)
+    );
+  }
+}));
+
+module.exports = RequestListColumnMethod;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-size.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getFormattedSize } = require("../utils/format-utils");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = ["contentSize"];
+
+/**
+ * Request list size column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnsize() {
+  let name = "size";
+  return (
+    Column({
+      cellRenderer: ContentSizeColumnCell,
+      className: "requests-list-subitem requests-list-size",
+      dataKey: name,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 72,
+      maxWidth: 72,
+      minWidth: 45,
+    })
+  );
+}
+
+RequestListColumnsize.displayName = "RequestListColumnsize";
+
+const ContentSizeColumnCell = createFactory(createClass({
+  displayName: "ContentSizeColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData } = this.props;
+    let { contentSize } = rowData;
+    let size = typeof contentSize === "number" ? getFormattedSize(contentSize) : null;
+    return size ? div({ title: size }, size) : null;
+  }
+}));
+
+module.exports = RequestListColumnsize;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-status.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div, span } = DOM;
+const UPDATED_PROPS = [
+  "fromCache",
+  "fromServiceWorker",
+  "status",
+  "statusText",
+];
+
+/**
+ * Request list status column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnStatus() {
+  let name = "status";
+  return (
+    Column({
+      cellRenderer: StatusColumnCell,
+      className: "requests-list-subitem requests-list-status",
+      dataKey: name,
+      disableSort: false,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name + "3",
+      width: 72,
+      maxWidth: 72,
+      minWidth: 50,
+    })
+  );
+}
+
+RequestListColumnStatus.displayName = "RequestListColumnStatus";
+
+const StatusColumnCell = createFactory(createClass({
+  displayName: "StatusColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData } = this.props;
+    let { status, statusText, fromCache, fromServiceWorker } = rowData;
+    let code, title;
+
+    if (status) {
+      if (fromCache) {
+        code = "cached";
+      } else if (fromServiceWorker) {
+        code = "service worker";
+      } else {
+        code = status;
+      }
+
+      if (statusText) {
+        title = `${status} ${statusText}`;
+        if (fromCache) {
+          title += " (cached)";
+        }
+        if (fromServiceWorker) {
+          title += " (service worker)";
+        }
+      }
+    }
+
+    return (
+      div({ title },
+        span({ className: "requests-list-status-icon", "data-code": code }),
+        span({ className: "requests-list-status-code subitem-label" }, status),
+      )
+    );
+  }
+}));
+
+module.exports = RequestListColumnStatus;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-transferred.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { getFormattedSize } = require("../utils/format-utils");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+  "fromCache",
+  "fromServiceWorker",
+  "transferredSize",
+];
+
+/**
+ * Request list transferred column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnTransferred() {
+  let name = "transferred";
+  return (
+    Column({
+      cellRenderer: TransferredSizeColumnCell,
+      className: "requests-list-subitem requests-list-transferred",
+      dataKey: name,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 96,
+      maxWidth: 96,
+      minWidth: 65,
+    })
+  );
+}
+
+RequestListColumnTransferred.displayName = "RequestListColumnTransferred";
+
+const TransferredSizeColumnCell = createFactory(createClass({
+  displayName: "TransferredSizeColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData } = this.props;
+    let { transferredSize, fromCache, fromServiceWorker, status } = rowData;
+
+    let title;
+    let className = "subitem-label";
+    if (fromCache || status === "304") {
+      title = L10N.getStr("networkMenu.sizeCached");
+      className += " theme-comment";
+    } else if (fromServiceWorker) {
+      title = L10N.getStr("networkMenu.sizeServiceWorker");
+      className += " theme-comment";
+    } else if (typeof transferredSize === "number") {
+      title = getFormattedSize(transferredSize);
+    } else if (transferredSize === null) {
+      title = L10N.getStr("networkMenu.sizeUnavailable");
+    }
+
+    return div({ className, title }, title);
+  }
+}));
+
+module.exports = RequestListColumnTransferred;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-type.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const {
+  getAbbreviatedMimeType,
+  propertiesEqual,
+} = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+  "ecmascript": "js",
+  "javascript": "js",
+  "x-javascript": "js"
+};
+const UPDATED_PROPS = ["mimeType"];
+
+/**
+ * Request list type column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnType() {
+  let name = "type";
+  return (
+    Column({
+      cellRenderer: TypeColumnCell,
+      className: "requests-list-subitem requests-list-type",
+      dataKey: name,
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 72,
+      maxWidth: 72,
+      minWidth: 45,
+    })
+  );
+}
+
+RequestListColumnType.displayName = "RequestListColumnType";
+
+const TypeColumnCell = createFactory(createClass({
+  displayName: "TypeColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData } = this.props;
+    let { mimeType } = rowData;
+    let abbrevType;
+
+    if (mimeType) {
+      abbrevType = getAbbreviatedMimeType(mimeType);
+      abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
+    }
+
+    return div({ title: mimeType }, abbrevType);
+  }
+}));
+
+module.exports = RequestListColumnType;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-waterfall.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+  "eventTimings",
+  "fromCache",
+  "fromServiceWorker",
+  "totalTime",
+];
+
+/**
+ * Request list waterfall column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnWaterfall(columnData) {
+  let name = "waterfall";
+  let { firstRequestStartedMillis } = columnData;
+  return (
+    Column({
+      cellRenderer: (props) =>
+        WaterfallColumnCell(Object.assign(props, { firstRequestStartedMillis })),
+      className: "requests-list-subitem requests-list-waterfall",
+      columnData,
+      dataKey: name,
+      flexGrow: 1,
+      headerClassName: "requests-list-waterfall",
+      headerRenderer: (props) => RequestListColumnHeader(props),
+      label: name,
+      width: 300,
+    })
+  );
+}
+
+RequestListColumnWaterfall.displayName = "RequestListColumnWaterfall";
+
+const WaterfallColumnCell = createFactory(createClass({
+  displayName: "WaterfallColumnCell",
+
+  propTypes: {
+    rowData: PropTypes.object,
+    firstRequestStartedMillis: PropTypes.number,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+  },
+
+  render() {
+    let { rowData, firstRequestStartedMillis } = this.props;
+    return (
+      div({
+        className: "requests-list-timings",
+        style: {
+          paddingInlineStart: `${rowData.startedMillis - firstRequestStartedMillis}px`,
+        },
+      },
+        timingBoxes(rowData),
+      )
+    );
+  }
+}));
+
+// List of properties of the timing info we want to create boxes for
+const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
+
+function timingBoxes(item) {
+  const { eventTimings, totalTime, fromCache, fromServiceWorker } = item;
+  let boxes = [];
+
+  if (fromCache || fromServiceWorker) {
+    return boxes;
+  }
+
+  if (eventTimings) {
+    // Add a set of boxes representing timing information.
+    for (let key of TIMING_KEYS) {
+      let width = eventTimings.timings[key];
+
+      // Don't render anything if it surely won't be visible.
+      // One millisecond == one unscaled pixel.
+      if (width > 0) {
+        boxes.push(div({
+          key,
+          className: "requests-list-timings-box " + key,
+          style: { width }
+        }));
+      }
+    }
+  }
+
+  if (typeof totalTime === "number") {
+    let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
+    boxes.push(
+      div({
+        key: "total",
+        className: "requests-list-timings-total",
+        title: text,
+      }, text)
+    );
+  }
+
+  return boxes;
+}
+
+module.exports = RequestListColumnWaterfall;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-content.js
+++ /dev/null
@@ -1,280 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { KeyCodes } = require("devtools/client/shared/keycodes");
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
-const Actions = require("../actions/index");
-const {
-  setTooltipImageContent,
-  setTooltipStackTraceContent,
-} = require("../request-list-tooltip");
-const {
-  getDisplayedRequests,
-  getWaterfallScale,
-} = require("../selectors/index");
-
-// Components
-const RequestListItem = createFactory(require("./request-list-item"));
-const RequestListContextMenu = require("../request-list-context-menu");
-
-const { div } = DOM;
-
-// tooltip show/hide delay in ms
-const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
-
-/**
- * Renders the actual contents of the request list.
- */
-const RequestListContent = createClass({
-  displayName: "RequestListContent",
-
-  propTypes: {
-    dispatch: PropTypes.func.isRequired,
-    displayedRequests: PropTypes.object.isRequired,
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    fromCache: PropTypes.bool.isRequired,
-    onItemMouseDown: PropTypes.func.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-    onSelectDelta: PropTypes.func.isRequired,
-    scale: PropTypes.number,
-    selectedRequestId: PropTypes.string,
-  },
-
-  componentWillMount() {
-    const { dispatch } = this.props;
-    this.contextMenu = new RequestListContextMenu({
-      cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-      openStatistics: (open) => dispatch(Actions.openStatistics(open)),
-    });
-    this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
-  },
-
-  componentDidMount() {
-    // Set the CSS variables for waterfall scaling
-    this.setScalingStyles();
-
-    // Install event handler for displaying a tooltip
-    this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
-      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
-      interactive: true
-    });
-
-    // Install event handler to hide the tooltip on scroll
-    this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
-  },
-
-  componentWillUpdate(nextProps) {
-    // Check if the list is scrolled to bottom before the UI update.
-    // The scroll is ever needed only if new rows are added to the list.
-    const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
-    this.shouldScrollBottom = delta > 0 && this.isScrolledToBottom();
-  },
-
-  componentDidUpdate(prevProps) {
-    // Update the CSS variables for waterfall scaling after props change
-    this.setScalingStyles(prevProps);
-
-    // Keep the list scrolled to bottom if a new row was added
-    if (this.shouldScrollBottom) {
-      let node = this.refs.contentEl;
-      node.scrollTop = node.scrollHeight;
-    }
-  },
-
-  componentWillUnmount() {
-    this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
-
-    // Uninstall the tooltip event handler
-    this.tooltip.stopTogglingOnHover();
-  },
-
-  /**
-   * Set the CSS variables for waterfall scaling. If React supported setting CSS
-   * variables as part of the "style" property of a DOM element, we would use that.
-   *
-   * However, React doesn't support this, so we need to use a hack and update the
-   * DOM element directly: https://github.com/facebook/react/issues/6411
-   */
-  setScalingStyles(prevProps) {
-    const { scale } = this.props;
-    if (prevProps && prevProps.scale === scale) {
-      return;
-    }
-
-    const { style } = this.refs.contentEl;
-    style.removeProperty("--timings-scale");
-    style.removeProperty("--timings-rev-scale");
-    style.setProperty("--timings-scale", scale);
-    style.setProperty("--timings-rev-scale", 1 / scale);
-  },
-
-  isScrolledToBottom() {
-    const { contentEl } = this.refs;
-    const lastChildEl = contentEl.lastElementChild;
-
-    if (!lastChildEl) {
-      return false;
-    }
-
-    let lastChildRect = lastChildEl.getBoundingClientRect();
-    let contentRect = contentEl.getBoundingClientRect();
-
-    return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom;
-  },
-
-  /**
-   * The predicate used when deciding whether a popup should be shown
-   * over a request item or not.
-   *
-   * @param nsIDOMNode target
-   *        The element node currently being hovered.
-   * @param object tooltip
-   *        The current tooltip instance.
-   * @return {Promise}
-   */
-  onHover(target, tooltip) {
-    let itemEl = target.closest(".request-list-item");
-    if (!itemEl) {
-      return false;
-    }
-    let itemId = itemEl.dataset.id;
-    if (!itemId) {
-      return false;
-    }
-    let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
-    if (!requestItem) {
-      return false;
-    }
-
-    if (requestItem.responseContent && target.closest(".requests-list-icon-and-file")) {
-      return setTooltipImageContent(tooltip, itemEl, requestItem);
-    } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
-      return setTooltipStackTraceContent(tooltip, requestItem);
-    }
-
-    return false;
-  },
-
-  /**
-   * Scroll listener for the requests menu view.
-   */
-  onScroll() {
-    this.tooltip.hide();
-  },
-
-  /**
-   * Handler for keyboard events. For arrow up/down, page up/down, home/end,
-   * move the selection up or down.
-   */
-  onKeyDown(e) {
-    let delta;
-
-    switch (e.keyCode) {
-      case KeyCodes.DOM_VK_UP:
-      case KeyCodes.DOM_VK_LEFT:
-        delta = -1;
-        break;
-      case KeyCodes.DOM_VK_DOWN:
-      case KeyCodes.DOM_VK_RIGHT:
-        delta = +1;
-        break;
-      case KeyCodes.DOM_VK_PAGE_UP:
-        delta = "PAGE_UP";
-        break;
-      case KeyCodes.DOM_VK_PAGE_DOWN:
-        delta = "PAGE_DOWN";
-        break;
-      case KeyCodes.DOM_VK_HOME:
-        delta = -Infinity;
-        break;
-      case KeyCodes.DOM_VK_END:
-        delta = +Infinity;
-        break;
-    }
-
-    if (delta) {
-      // Prevent scrolling when pressing navigation keys.
-      e.preventDefault();
-      e.stopPropagation();
-      this.props.onSelectDelta(delta);
-    }
-  },
-
-  onContextMenu(evt) {
-    evt.preventDefault();
-    this.contextMenu.open(evt);
-  },
-
-  /**
-   * If selection has just changed (by keyboard navigation), don't keep the list
-   * scrolled to bottom, but allow scrolling up with the selection.
-   */
-  onFocusedNodeChange() {
-    this.shouldScrollBottom = false;
-  },
-
-  render() {
-    const {
-      displayedRequests,
-      firstRequestStartedMillis,
-      selectedRequestId,
-      onItemMouseDown,
-      onSecurityIconClick,
-    } = this.props;
-
-    return (
-      div({
-        ref: "contentEl",
-        className: "requests-list-contents",
-        tabIndex: 0,
-        onKeyDown: this.onKeyDown,
-      },
-        displayedRequests.map((item, index) => RequestListItem({
-          firstRequestStartedMillis,
-          fromCache: item.status === "304" || item.fromCache,
-          item,
-          index,
-          isSelected: item.id === selectedRequestId,
-          key: item.id,
-          onContextMenu: this.onContextMenu,
-          onFocusedNodeChange: this.onFocusedNodeChange,
-          onMouseDown: () => onItemMouseDown(item.id),
-          onSecurityIconClick: () => onSecurityIconClick(item.securityState),
-        }))
-      )
-    );
-  },
-});
-
-module.exports = connect(
-  (state) => ({
-    displayedRequests: getDisplayedRequests(state),
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
-    selectedRequestId: state.requests.selectedId,
-    scale: getWaterfallScale(state),
-  }),
-  (dispatch) => ({
-    dispatch,
-    onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
-    /**
-     * A handler that opens the security tab in the details view if secure or
-     * broken security indicator is clicked.
-     */
-    onSecurityIconClick: (securityState) => {
-      if (securityState && securityState !== "insecure") {
-        dispatch(Actions.selectDetailsPanelTab("security"));
-      }
-    },
-    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
-  }),
-)(RequestListContent);
--- a/devtools/client/netmonitor/components/request-list-empty.js
+++ b/devtools/client/netmonitor/components/request-list-empty.js
@@ -1,70 +1,55 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
-  createClass,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const Actions = require("../actions/index");
-const { ACTIVITY_TYPE } = require("../constants");
 const { L10N } = require("../utils/l10n");
 
 const { button, div, span } = DOM;
 
 /**
  * UI displayed when the request list is empty. Contains instructions on reloading
  * the page and on triggering performance analysis of the page.
  */
-const RequestListEmptyNotice = createClass({
-  displayName: "RequestListEmptyNotice",
-
-  propTypes: {
-    onReloadClick: PropTypes.func.isRequired,
-    onPerfClick: PropTypes.func.isRequired,
-  },
-
-  render() {
-    return div(
-      {
-        className: "request-list-empty-notice",
-      },
+function RequestListEmptyNotice({
+  onPerfClick,
+  onReloadClick,
+}) {
+  return (
+    div({ className: "request-list-empty-notice" },
       div({ className: "notice-reload-message" },
         span(null, L10N.getStr("netmonitor.reloadNotice1")),
-        button(
-          {
-            className: "devtools-toolbarbutton requests-list-reload-notice-button",
-            "data-standalone": true,
-            onClick: this.props.onReloadClick,
-          },
-          L10N.getStr("netmonitor.reloadNotice2")
-        ),
+        button({
+          className: "devtools-button requests-list-reload-notice-button",
+          "data-standalone": true,
+          onClick: onReloadClick,
+        }, L10N.getStr("netmonitor.reloadNotice2")),
         span(null, L10N.getStr("netmonitor.reloadNotice3"))
       ),
       div({ className: "notice-perf-message" },
         span(null, L10N.getStr("netmonitor.perfNotice1")),
         button({
           title: L10N.getStr("netmonitor.perfNotice3"),
           className: "devtools-button requests-list-perf-notice-button",
           "data-standalone": true,
-          onClick: this.props.onPerfClick,
+          onClick: onPerfClick,
         }),
         span(null, L10N.getStr("netmonitor.perfNotice2"))
       )
-    );
-  }
-});
+    )
+  );
+}
+
+RequestListEmptyNotice.displayName = "RequestListEmptyNotice";
 
-module.exports = connect(
-  undefined,
-  dispatch => ({
-    onPerfClick: () => dispatch(Actions.openStatistics(true)),
-    onReloadClick: () =>
-      window.NetMonitorController
-        .triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT),
-  })
-)(RequestListEmptyNotice);
+RequestListEmptyNotice.propTypes = {
+  onPerfClick: PropTypes.func.isRequired,
+  onReloadClick: PropTypes.func.isRequired,
+};
+
+module.exports = RequestListEmptyNotice;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-header.js
+++ /dev/null
@@ -1,200 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
-const { div, button } = DOM;
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
-const { L10N } = require("../utils/l10n");
-const { getWaterfallScale } = require("../selectors/index");
-const Actions = require("../actions/index");
-const WaterfallBackground = require("../waterfall-background");
-const { getFormattedTime } = require("../utils/format-utils");
-
-// ms
-const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
-// px
-const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
-
-const HEADERS = [
-  { name: "status", label: "status3" },
-  { name: "method" },
-  { name: "file", boxName: "icon-and-file" },
-  { name: "domain", boxName: "security-and-domain" },
-  { name: "cause" },
-  { name: "type" },
-  { name: "transferred" },
-  { name: "size" },
-  { name: "waterfall" }
-];
-
-/**
- * Render the request list header with sorting arrows for columns.
- * Displays tick marks in the waterfall column header.
- * Also draws the waterfall background canvas and updates it when needed.
- */
-const RequestListHeader = createClass({
-  displayName: "RequestListHeader",
-
-  propTypes: {
-    sort: PropTypes.object,
-    scale: PropTypes.number,
-    waterfallWidth: PropTypes.number,
-    onHeaderClick: PropTypes.func.isRequired,
-    resizeWaterfall: PropTypes.func.isRequired,
-  },
-
-  componentDidMount() {
-    // Create the object that takes care of drawing the waterfall canvas background
-    this.background = new WaterfallBackground(document);
-    this.background.draw(this.props);
-    this.resizeWaterfall();
-    window.addEventListener("resize", this.resizeWaterfall);
-  },
-
-  componentDidUpdate() {
-    this.background.draw(this.props);
-  },
-
-  componentWillUnmount() {
-    this.background.destroy();
-    this.background = null;
-    window.removeEventListener("resize", this.resizeWaterfall);
-  },
-
-  resizeWaterfall() {
-    // Measure its width and update the 'waterfallWidth' property in the store.
-    // The 'waterfallWidth' will be further updated on every window resize.
-    setNamedTimeout("resize-events", 50, () => {
-      const { width } = this.refs.header.getBoundingClientRect();
-      this.props.resizeWaterfall(width);
-    });
-  },
-
-  render() {
-    const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
-
-    return div(
-      { className: "devtools-toolbar requests-list-toolbar" },
-      div({ className: "toolbar-labels" },
-        HEADERS.map(header => {
-          const name = header.name;
-          const boxName = header.boxName || name;
-          const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
-
-          let sorted, sortedTitle;
-          const active = sort.type == name ? true : undefined;
-          if (active) {
-            sorted = sort.ascending ? "ascending" : "descending";
-            sortedTitle = L10N.getStr(sort.ascending
-              ? "networkMenu.sortedAsc"
-              : "networkMenu.sortedDesc");
-          }
-
-          return div(
-            {
-              id: `requests-list-${boxName}-header-box`,
-              className: `requests-list-header requests-list-${boxName}`,
-              key: name,
-              ref: "header",
-              // Used to style the next column.
-              "data-active": active,
-            },
-            button(
-              {
-                id: `requests-list-${name}-button`,
-                className: `requests-list-header-button requests-list-${name}`,
-                "data-sorted": sorted,
-                title: sortedTitle,
-                onClick: () => onHeaderClick(name),
-              },
-              name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label)
-                                  : div({ className: "button-text" }, label),
-              div({ className: "button-icon" })
-            )
-          );
-        })
-      )
-    );
-  }
-});
-
-/**
- * Build the waterfall header - timing tick marks with the right spacing
- */
-function waterfallDivisionLabels(waterfallWidth, scale) {
-  let labels = [];
-
-  // Build new millisecond tick labels...
-  let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
-  let scaledStep = scale * timingStep;
-
-  // Ignore any divisions that would end up being too close to each other.
-  while (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
-    scaledStep *= 2;
-  }
-
-  // Insert one label for each division on the current scale.
-  for (let x = 0; x < waterfallWidth; x += scaledStep) {
-    let millisecondTime = x / scale;
-    let divisionScale = "millisecond";
-
-    // If the division is greater than 1 minute.
-    if (millisecondTime > 60000) {
-      divisionScale = "minute";
-    } else if (millisecondTime > 1000) {
-      // If the division is greater than 1 second.
-      divisionScale = "second";
-    }
-
-    let width = (x + scaledStep | 0) - (x | 0);
-    // Adjust the first marker for the borders
-    if (x == 0) {
-      width -= 2;
-    }
-    // Last marker doesn't need a width specified at all
-    if (x + scaledStep >= waterfallWidth) {
-      width = undefined;
-    }
-
-    labels.push(div(
-      {
-        key: labels.length,
-        className: "requests-list-timings-division",
-        "data-division-scale": divisionScale,
-        style: { width }
-      },
-      getFormattedTime(millisecondTime)
-    ));
-  }
-
-  return labels;
-}
-
-function WaterfallLabel(waterfallWidth, scale, label) {
-  let className = "button-text requests-list-waterfall-label-wrapper";
-
-  if (waterfallWidth != null && scale != null) {
-    label = waterfallDivisionLabels(waterfallWidth, scale);
-    className += " requests-list-waterfall-visible";
-  }
-
-  return div({ className }, label);
-}
-
-module.exports = connect(
-  state => ({
-    sort: state.sort,
-    scale: getWaterfallScale(state),
-    waterfallWidth: state.ui.waterfallWidth,
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
-    timingMarkers: state.timingMarkers,
-  }),
-  dispatch => ({
-    onHeaderClick: type => dispatch(Actions.sortBy(type)),
-    resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
-  })
-)(RequestListHeader);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-item.js
+++ /dev/null
@@ -1,528 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../utils/l10n");
-const { getAbbreviatedMimeType } = require("../utils/request-utils");
-const { getFormattedSize } = require("../utils/format-utils");
-
-const { div, img, span } = DOM;
-
-/**
- * Compare two objects on a subset of their properties
- */
-function propertiesEqual(props, item1, item2) {
-  return item1 === item2 || props.every(p => item1[p] === item2[p]);
-}
-
-/**
- * Used by shouldComponentUpdate: compare two items, and compare only properties
- * relevant for rendering the RequestListItem. Other properties (like request and
- * response headers, cookies, bodies) are ignored. These are very useful for the
- * network details, but not here.
- */
-const UPDATED_REQ_ITEM_PROPS = [
-  "mimeType",
-  "eventTimings",
-  "securityState",
-  "responseContentDataUri",
-  "status",
-  "statusText",
-  "fromCache",
-  "fromServiceWorker",
-  "method",
-  "url",
-  "remoteAddress",
-  "cause",
-  "contentSize",
-  "transferredSize",
-  "startedMillis",
-  "totalTime",
-];
-
-const UPDATED_REQ_PROPS = [
-  "index",
-  "isSelected",
-  "firstRequestStartedMillis",
-];
-
-/**
- * Render one row in the request list.
- */
-const RequestListItem = createClass({
-  displayName: "RequestListItem",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-    index: PropTypes.number.isRequired,
-    isSelected: PropTypes.bool.isRequired,
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    fromCache: PropTypes.bool.isRequired,
-    onContextMenu: PropTypes.func.isRequired,
-    onFocusedNodeChange: PropTypes.func,
-    onMouseDown: PropTypes.func.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-  },
-
-  componentDidMount() {
-    if (this.props.isSelected) {
-      this.refs.el.focus();
-    }
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
-      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
-  },
-
-  componentDidUpdate(prevProps) {
-    if (!prevProps.isSelected && this.props.isSelected) {
-      this.refs.el.focus();
-      if (this.props.onFocusedNodeChange) {
-        this.props.onFocusedNodeChange();
-      }
-    }
-  },
-
-  render() {
-    const {
-      item,
-      index,
-      isSelected,
-      firstRequestStartedMillis,
-      fromCache,
-      onContextMenu,
-      onMouseDown,
-      onSecurityIconClick
-    } = this.props;
-
-    let classList = ["request-list-item"];
-    if (isSelected) {
-      classList.push("selected");
-    }
-
-    if (fromCache) {
-      classList.push("fromCache");
-    }
-
-    classList.push(index % 2 ? "odd" : "even");
-
-    return (
-      div({
-        ref: "el",
-        className: classList.join(" "),
-        "data-id": item.id,
-        tabIndex: 0,
-        onContextMenu,
-        onMouseDown,
-      },
-        StatusColumn({ item }),
-        MethodColumn({ item }),
-        FileColumn({ item }),
-        DomainColumn({ item, onSecurityIconClick }),
-        CauseColumn({ item }),
-        TypeColumn({ item }),
-        TransferredSizeColumn({ item }),
-        ContentSizeColumn({ item }),
-        WaterfallColumn({ item, firstRequestStartedMillis }),
-      )
-    );
-  }
-});
-
-const UPDATED_STATUS_PROPS = [
-  "status",
-  "statusText",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const StatusColumn = createFactory(createClass({
-  displayName: "StatusColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { status, statusText, fromCache, fromServiceWorker } = this.props.item;
-
-    let code, title;
-
-    if (status) {
-      if (fromCache) {
-        code = "cached";
-      } else if (fromServiceWorker) {
-        code = "service worker";
-      } else {
-        code = status;
-      }
-
-      if (statusText) {
-        title = `${status} ${statusText}`;
-        if (fromCache) {
-          title += " (cached)";
-        }
-        if (fromServiceWorker) {
-          title += " (service worker)";
-        }
-      }
-    }
-
-    return (
-        div({ className: "requests-list-subitem requests-list-status", title },
-        div({ className: "requests-list-status-icon", "data-code": code }),
-        span({ className: "subitem-label requests-list-status-code" }, status)
-      )
-    );
-  }
-}));
-
-const MethodColumn = createFactory(createClass({
-  displayName: "MethodColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.method !== nextProps.item.method;
-  },
-
-  render() {
-    const { method } = this.props.item;
-    return (
-      div({ className: "requests-list-subitem requests-list-method-box" },
-        span({ className: "subitem-label requests-list-method" }, method)
-      )
-    );
-  }
-}));
-
-const UPDATED_FILE_PROPS = [
-  "urlDetails",
-  "responseContentDataUri",
-];
-
-const FileColumn = createFactory(createClass({
-  displayName: "FileColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { urlDetails, responseContentDataUri } = this.props.item;
-
-    return (
-      div({ className: "requests-list-subitem requests-list-icon-and-file" },
-        img({
-          className: "requests-list-icon",
-          src: responseContentDataUri,
-          hidden: !responseContentDataUri,
-          "data-type": responseContentDataUri ? "thumbnail" : undefined,
-        }),
-        div({
-          className: "subitem-label requests-list-file",
-          title: urlDetails.unicodeUrl,
-        },
-          urlDetails.baseNameWithQuery,
-        ),
-      )
-    );
-  }
-}));
-
-const UPDATED_DOMAIN_PROPS = [
-  "urlDetails",
-  "remoteAddress",
-  "securityState",
-];
-
-const DomainColumn = createFactory(createClass({
-  displayName: "DomainColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { item, onSecurityIconClick } = this.props;
-    const { urlDetails, remoteAddress, securityState } = item;
-
-    let iconClassList = ["requests-security-state-icon"];
-    let iconTitle;
-    if (urlDetails.isLocal) {
-      iconClassList.push("security-state-local");
-      iconTitle = L10N.getStr("netmonitor.security.state.secure");
-    } else if (securityState) {
-      iconClassList.push(`security-state-${securityState}`);
-      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
-    }
-
-    let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
-
-    return (
-      div({ className: "requests-list-subitem requests-list-security-and-domain" },
-        div({
-          className: iconClassList.join(" "),
-          title: iconTitle,
-          onClick: onSecurityIconClick,
-        }),
-        span({ className: "subitem-label requests-list-domain", title }, urlDetails.host),
-      )
-    );
-  }
-}));
-
-const CauseColumn = createFactory(createClass({
-  displayName: "CauseColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.cause !== nextProps.item.cause;
-  },
-
-  render() {
-    const { cause } = this.props.item;
-
-    let causeType = "";
-    let causeUri = undefined;
-    let causeHasStack = false;
-
-    if (cause) {
-      // Legacy server might send a numeric value. Display it as "unknown"
-      causeType = typeof cause.type === "string" ? cause.type : "unknown";
-      causeUri = cause.loadingDocumentUri;
-      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-cause",
-        title: causeUri,
-      },
-        span({
-          className: "requests-list-cause-stack",
-          hidden: !causeHasStack,
-        }, "JS"),
-        span({ className: "subitem-label" }, causeType),
-      )
-    );
-  }
-}));
-
-const CONTENT_MIME_TYPE_ABBREVIATIONS = {
-  "ecmascript": "js",
-  "javascript": "js",
-  "x-javascript": "js"
-};
-
-const TypeColumn = createFactory(createClass({
-  displayName: "TypeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.mimeType !== nextProps.item.mimeType;
-  },
-
-  render() {
-    const { mimeType } = this.props.item;
-    let abbrevType;
-    if (mimeType) {
-      abbrevType = getAbbreviatedMimeType(mimeType);
-      abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-type",
-        title: mimeType,
-      },
-        span({ className: "subitem-label" }, abbrevType),
-      )
-    );
-  }
-}));
-
-const UPDATED_TRANSFERRED_PROPS = [
-  "transferredSize",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const TransferredSizeColumn = createFactory(createClass({
-  displayName: "TransferredSizeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { transferredSize, fromCache, fromServiceWorker, status } = this.props.item;
-
-    let text;
-    let className = "subitem-label";
-    if (fromCache || status === "304") {
-      text = L10N.getStr("networkMenu.sizeCached");
-      className += " theme-comment";
-    } else if (fromServiceWorker) {
-      text = L10N.getStr("networkMenu.sizeServiceWorker");
-      className += " theme-comment";
-    } else if (typeof transferredSize == "number") {
-      text = getFormattedSize(transferredSize);
-    } else if (transferredSize === null) {
-      text = L10N.getStr("networkMenu.sizeUnavailable");
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-transferred",
-        title: text,
-      },
-        span({ className }, text),
-      )
-    );
-  }
-}));
-
-const ContentSizeColumn = createFactory(createClass({
-  displayName: "ContentSizeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.contentSize !== nextProps.item.contentSize;
-  },
-
-  render() {
-    const { contentSize } = this.props.item;
-
-    let text;
-    if (typeof contentSize == "number") {
-      text = getFormattedSize(contentSize);
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem subitem-label requests-list-size",
-        title: text,
-      },
-        span({ className: "subitem-label" }, text),
-      )
-    );
-  }
-}));
-
-const UPDATED_WATERFALL_PROPS = [
-  "eventTimings",
-  "totalTime",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const WaterfallColumn = createFactory(createClass({
-  displayName: "WaterfallColumn",
-
-  propTypes: {
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis ||
-      !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { item, firstRequestStartedMillis } = this.props;
-
-    return (
-      div({ className: "requests-list-subitem requests-list-waterfall" },
-        div({
-          className: "requests-list-timings",
-          style: {
-            paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`,
-          },
-        },
-          timingBoxes(item),
-        )
-      )
-    );
-  }
-}));
-
-// List of properties of the timing info we want to create boxes for
-const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
-
-function timingBoxes(item) {
-  const { eventTimings, totalTime, fromCache, fromServiceWorker } = item;
-  let boxes = [];
-
-  if (fromCache || fromServiceWorker) {
-    return boxes;
-  }
-
-  if (eventTimings) {
-    // Add a set of boxes representing timing information.
-    for (let key of TIMING_KEYS) {
-      let width = eventTimings.timings[key];
-
-      // Don't render anything if it surely won't be visible.
-      // One millisecond == one unscaled pixel.
-      if (width > 0) {
-        boxes.push(div({
-          key,
-          className: "requests-list-timings-box " + key,
-          style: { width }
-        }));
-      }
-    }
-  }
-
-  if (typeof totalTime === "number") {
-    let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
-    boxes.push(div({
-      key: "total",
-      className: "requests-list-timings-total",
-      title: text
-    }, text));
-  }
-
-  return boxes;
-}
-
-module.exports = RequestListItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-row.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { div } = DOM;
+
+/**
+ * Request list row component
+ */
+function RequestListRow({
+  className,
+  columns,
+  index,
+  key,
+  onRowClick,
+  rowData,
+  style,
+}) {
+  return (
+    div({
+      "aria-label": "row",
+      "data-id": rowData.id,
+      role: "row",
+      className: rowData.fromCache ? `${className} fromCache` : className,
+      key,
+      onMouseDown: () => onRowClick({ index }),
+      style,
+    },
+      columns
+    )
+  );
+}
+
+RequestListRow.displayName = "RequestListRow";
+
+RequestListRow.propTypes = {
+  className: PropTypes.string.isRequired,
+  columns: PropTypes.object.isRequired,
+  index: PropTypes.number.isRequired,
+  key: PropTypes.string.isRequired,
+  onRowClick: PropTypes.func.isRequired,
+  onRowContextMenu: PropTypes.func.isRequired,
+  rowData: PropTypes.object.isRequired,
+  style: PropTypes.object.isRequired,
+};
+
+module.exports = RequestListRow;
--- a/devtools/client/netmonitor/components/request-list.js
+++ b/devtools/client/netmonitor/components/request-list.js
@@ -1,38 +1,328 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
+  createClass,
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const SortDirection = require("devtools/client/shared/vendor/react-virtualized").SortDirection;
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const Actions = require("../actions/index");
+const { ACTIVITY_TYPE } = require("../constants");
+const RequestListContextMenu = require("../request-list-context-menu");
+const { setTooltipStackTraceContent } = require("../request-list-tooltip");
+const {
+  getDisplayedRequests,
+  getSortedRequests,
+  getWaterfallScale,
+} = require("../selectors/index");
 
 // Components
-const RequestListContent = createFactory(require("./request-list-content"));
-const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
-const RequestListHeader = createFactory(require("./request-list-header"));
+const AutoSizer = createFactory(require("devtools/client/shared/vendor/react-virtualized").AutoSizer);
+const Table = createFactory(require("devtools/client/shared/vendor/react-virtualized").Table);
+const RequestListColumnCause = require("./request-list-column-cause");
+const RequestListColumnDomain = require("./request-list-column-domain");
+const RequestListColumnFile = require("./request-list-column-file");
+const RequestListColumnMethod = require("./request-list-column-method");
+const RequestListColumnSize = require("./request-list-column-size");
+const RequestListColumnStatus = require("./request-list-column-status");
+const RequestListColumnTransferred = require("./request-list-column-transferred");
+const RequestListColumnType = require("./request-list-column-type");
+const RequestListColumnWaterfall = require("./request-list-column-waterfall");
+const RequestListEmpty = createFactory(require("./request-list-empty"));
+const RequestListRow = createFactory(require("./request-list-row"));
 
 const { div } = DOM;
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
 
 /**
- * Request panel component
+ * Request list component
  */
-function RequestList({ isEmpty }) {
-  return (
-    div({ className: "request-list-container" },
-      RequestListHeader(),
-      isEmpty ? RequestListEmptyNotice() : RequestListContent(),
-    )
-  );
-}
+const RequestList = createClass({
+  displayName: "RequestList",
+
+  propTypes: {
+    cloneSelectedRequest: PropTypes.func.isRequired,
+    displayedRequests: PropTypes.object.isRequired,
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    isEmpty: PropTypes.bool.isEmpty,
+    openStatistics: PropTypes.func.isRequired,
+    onPerfClick: PropTypes.func.isRequired,
+    onReloadClick: PropTypes.func.isRequired,
+    resizeWaterfall: PropTypes.func.isRequired,
+    scale: PropTypes.func.isRequired,
+    selectedRowIndex: PropTypes.number,
+    selectDelta: PropTypes.func.isRequired,
+    selectDetailsPanelTab: PropTypes.func.isRequired,
+    selectRequestByIndex: PropTypes.func.isRequired,
+    sort: PropTypes.func.isRequired,
+    timingMarkers: PropTypes.object.isRequired,
+    waterfallWidth: PropTypes.func.isRequired,
+  },
+
+  getInitialState() {
+    return {
+      sortBy: "index",
+      sortDirection: SortDirection.ASC,
+    };
+  },
+
+  componentWillMount() {
+    const { cloneSelectedRequest, openStatistics } = this.props;
+    this.contextMenu = new RequestListContextMenu({
+      cloneSelectedRequest,
+      openStatistics,
+    });
+    this.tooltip = new HTMLTooltip(document, { type: "arrow" });
+  },
+
+  componentDidMount() {
+    // Set the CSS variables for waterfall scaling
+    this.setScalingStyles();
+
+    // Install event handler for displaying a tooltip
+    this.tooltip.startTogglingOnHover(this.refs.list, this.onHover, {
+      toggleDelay: 500,
+      interactive: true
+    });
+
+    this.shouldScrollBottom = true;
+    this.shouldScrollToSelectedIndex = false;
+  },
+
+  componentWillUpdate(nextProps) {
+    let { selectedRowIndex } = nextProps;
+    if (selectedRowIndex !== -1 && selectedRowIndex !== this.props.selectedRowIndex) {
+      this.shouldScrollToSelectedIndex = true;
+    }
+  },
+
+  componentDidUpdate(prevProps) {
+    this.setScalingStyles(prevProps);
+  },
+
+  componentWillUnmount() {
+    this.tooltip.stopTogglingOnHover();
+  },
+
+  getRowClassName({ index }) {
+    if (index < 0) {
+      return "requests-list-toolbar devtools-toolbar";
+    }
+    let className = ["request-list-item"];
+    if (this.props.selectedRowIndex === index) {
+      className.push("selected");
+    }
+    className.push(index % 2 === 0 ? "even" : "odd");
+    return className.join(" ");
+  },
+
+  noRowsRenderer() {
+    let { isEmpty, onPerfClick, onReloadClick } = this.props;
+    return isEmpty ? RequestListEmpty({ onPerfClick, onReloadClick }) : null;
+  },
+
+  onContextMenu(evt) {
+    evt.preventDefault();
+    this.contextMenu.open(evt);
+  },
+
+  onHover(target, tooltip) {
+    let itemEl = target.closest(".request-list-item");
+    if (!itemEl) {
+      return false;
+    }
+
+    let itemId = itemEl.dataset.id;
+    if (!itemId) {
+      return false;
+    }
+
+    let requestItem = this.props.displayedRequests.find(r => r.id === itemId);
+    if (!requestItem) {
+      return false;
+    }
+
+    if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
+      return setTooltipStackTraceContent(tooltip, requestItem);
+    }
+
+    return false;
+  },
+
+  onKeyDown(evt) {
+    let { displayedRequests } = this.props;
+    let delta;
+
+    switch (evt.key) {
+      case "ArrowUp":
+      case "ArrowLeft":
+        delta = -1;
+        break;
+      case "ArrowDown":
+      case "ArrowRight":
+        delta = +1;
+        break;
+      case "PageUp":
+        delta = -Math.ceil(displayedRequests.size / PAGE_SIZE_ITEM_COUNT_RATIO);
+        break;
+      case "PageDown":
+        delta = Math.ceil(displayedRequests.size / PAGE_SIZE_ITEM_COUNT_RATIO);
+        break;
+      case "Home":
+        delta = -Infinity;
+        break;
+      case "End":
+        delta = +Infinity;
+        break;
+    }
 
-RequestList.displayName = "RequestList";
+    if (delta) {
+      // Prevent scrolling when pressing navigation keys.
+      evt.preventDefault();
+      evt.stopPropagation();
+      this.props.selectDelta(delta);
+    }
+  },
+
+  onRowClick({ index }) {
+    this.props.selectRequestByIndex(index);
+  },
+
+  onScroll({ clientHeight, scrollHeight, scrollTop }) {
+    this.shouldScrollToSelectedIndex = false;
+    this.shouldScrollBottom = clientHeight + scrollTop >= scrollHeight;
+    this.tooltip.hide();
+  },
+
+  /**
+   * Set the CSS variables for waterfall scaling. If React supported setting CSS
+   * variables as part of the "style" property of a DOM element, we would use that.
+   *
+   * However, React doesn't support this, so we need to use a hack and update the
+   * DOM element directly: https://github.com/facebook/react/issues/6411
+   */
+  setScalingStyles(prevProps) {
+    const { scale } = this.props;
+    if (prevProps && prevProps.scale === scale) {
+      return;
+    }
+
+    const { style } = this.refs.list;
+    style.removeProperty("--timings-scale");
+    style.removeProperty("--timings-rev-scale");
+    style.setProperty("--timings-scale", scale);
+    style.setProperty("--timings-rev-scale", 1 / scale);
+  },
+
+  sort({ sortBy, sortDirection }) {
+    this.props.sort(sortBy);
+    this.setState({ sortBy, sortDirection });
+  },
+
+  render() {
+    let {
+      displayedRequests,
+      firstRequestStartedMillis,
+      resizeWaterfall,
+      scale,
+      selectedRowIndex,
+      selectDetailsPanelTab,
+      timingMarkers,
+      waterfallWidth,
+    } = this.props;
+
+    let {
+      sortBy,
+      sortDirection,
+    } = this.state;
+
+    let scrollToIndex = -1;
+
+    if (this.shouldScrollToSelectedIndex) {
+      scrollToIndex = selectedRowIndex;
+    } else if (this.shouldScrollBottom) {
+      scrollToIndex = displayedRequests.size - 1;
+    }
 
-RequestList.propTypes = {
-  isEmpty: PropTypes.bool.isRequired,
-};
+    return (
+      div({
+        className: "requests-list-container",
+        onContextMenu: this.onContextMenu,
+        onKeyDown: this.onKeyDown,
+        ref: "list",
+      },
+        AutoSizer({},
+          ({ width, height }) => (
+            Table({
+              headerHeight: 24,
+              noRowsRenderer: this.noRowsRenderer,
+              onRowClick: this.onRowClick,
+              onScroll: this.onScroll,
+              overscanRowCount: 100,
+              rowClassName: this.getRowClassName,
+              rowCount: displayedRequests.size,
+              rowGetter: ({ index }) => displayedRequests.get(index),
+              rowHeight: 22,
+              rowRenderer: RequestListRow,
+              scrollToIndex,
+              sort: this.sort,
+              sortBy,
+              sortDirection,
+              width,
+              height,
+            },
+              RequestListColumnStatus(),
+              RequestListColumnMethod(),
+              RequestListColumnFile(),
+              RequestListColumnDomain({ selectDetailsPanelTab }),
+              RequestListColumnCause(),
+              RequestListColumnType(),
+              RequestListColumnTransferred(),
+              RequestListColumnSize(),
+              RequestListColumnWaterfall({
+                firstRequestStartedMillis,
+                scale,
+                resizeWaterfall,
+                timingMarkers,
+                waterfallWidth,
+              }),
+            )
+          )
+        )
+      )
+    );
+  }
+});
 
-module.exports = RequestList;
+module.exports = connect(
+  (state) => ({
+    displayedRequests: getDisplayedRequests(state),
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    isEmpty: state.requests.requests.isEmpty(),
+    scale: getWaterfallScale(state),
+    selectedRowIndex: getSortedRequests(state)
+      .findIndex(r => r.id === state.requests.selectedId),
+    timingMarkers: state.timingMarkers,
+    waterfallWidth: state.ui.waterfallWidth,
+  }),
+  (dispatch) => ({
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+    onReloadClick: () =>
+      window.NetMonitorController
+        .triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT),
+    onPerfClick: () => dispatch(Actions.openStatistics(true)),
+    openStatistics: (open) => dispatch(Actions.openStatistics(open)),
+    resizeWaterfall: (width) => dispatch(Actions.resizeWaterfall(width)),
+    sort: (type) => dispatch(Actions.sortBy(type)),
+    selectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
+    selectDetailsPanelTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
+    selectRequestByIndex: (index) => dispatch(Actions.selectRequestByIndex(index)),
+  }),
+)(RequestList);
--- a/devtools/client/netmonitor/reducers/ui.js
+++ b/devtools/client/netmonitor/reducers/ui.js
@@ -6,18 +6,18 @@
 
 const I = require("devtools/client/shared/vendor/immutable");
 const {
   CLEAR_REQUESTS,
   OPEN_NETWORK_DETAILS,
   OPEN_STATISTICS,
   REMOVE_SELECTED_CUSTOM_REQUEST,
   SELECT_DETAILS_PANEL_TAB,
+  SELECT_REQUEST,
   SEND_CUSTOM_REQUEST,
-  SELECT_REQUEST,
   WATERFALL_RESIZE,
 } = require("../constants");
 
 const UI = I.Record({
   detailsPanelSelectedTab: "headers",
   networkDetailsOpen: false,
   statisticsOpen: false,
   waterfallWidth: null,
--- a/devtools/client/netmonitor/request-list-tooltip.js
+++ b/devtools/client/netmonitor/request-list-tooltip.js
@@ -1,45 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {
-  setImageTooltip,
-  getImageDimensions,
-} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const { WEBCONSOLE_L10N } = require("./utils/l10n");
-const { formDataURI } = require("./utils/request-utils");
 
 // px
-const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
-// px
 const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
-async function setTooltipImageContent(tooltip, itemEl, requestItem) {
-  let { mimeType, text, encoding } = requestItem.responseContent.content;
-
-  if (!mimeType || !mimeType.includes("image/")) {
-    return false;
-  }
-
-  let string = await window.gNetwork.getString(text);
-  let src = formDataURI(mimeType, encoding, string);
-  let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
-  let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
-  let options = { maxDim, naturalWidth, naturalHeight };
-  setImageTooltip(tooltip, tooltip.doc, src, options);
-
-  return itemEl.querySelector(".requests-list-icon");
-}
-
 async function setTooltipStackTraceContent(tooltip, requestItem) {
   let {stacktrace} = requestItem.cause;
 
   if (!stacktrace || stacktrace.length == 0) {
     return false;
   }
 
   let doc = tooltip.doc;
@@ -96,11 +72,10 @@ async function setTooltipStackTraceConte
   }
 
   tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
 
   return true;
 }
 
 module.exports = {
-  setTooltipImageContent,
   setTooltipStackTraceContent,
 };
--- a/devtools/client/netmonitor/utils/request-utils.js
+++ b/devtools/client/netmonitor/utils/request-utils.js
@@ -217,23 +217,31 @@ function parseQueryString(query) {
     let param = e.split("=");
     return {
       name: param[0] ? decodeUnicodeUrl(param[0]) : "",
       value: param[1] ? decodeUnicodeUrl(param[1]) : "",
     };
   });
 }
 
+/**
+ * Compare two objects on a subset of their properties
+ */
+function propertiesEqual(props, item1, item2) {
+  return item1 === item2 || props.every(p => item1[p] === item2[p]);
+}
+
 module.exports = {
   getFormDataSections,
   fetchHeaders,
   formDataURI,
   writeHeaderText,
   decodeUnicodeUrl,
   getAbbreviatedMimeType,
   getUrlBaseName,
   getUrlQuery,
   getUrlBaseNameWithQuery,
   getUrlHostName,
   getUrlHost,
   getUrlDetails,
   parseQueryString,
+  propertiesEqual,
 };
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -6,43 +6,16 @@
 @import "resource://devtools/client/shared/components/tree/tree-view.css";
 @import "resource://devtools/client/shared/components/tabs/tabs.css";
 @import "resource://devtools/client/shared/components/tabs/tabbar.css";
 
 * {
   box-sizing: border-box;
 }
 
-.toolbar-labels {
-  overflow: hidden;
-  display: flex;
-  flex: auto;
-}
-
-.devtools-toolbar-container {
-  display: flex;
-  justify-content: space-between;
-}
-
-.devtools-toolbar-group {
-  display: flex;
-  flex: 0 0 auto;
-  flex-wrap: nowrap;
-  align-items: center;
-}
-
-#response-content-image-box {
-  overflow: auto;
-}
-
-.cropped-textbox .textbox-input {
-  /* workaround for textbox not supporting the @crop attribute */
-  text-overflow: ellipsis;
-}
-
 :root.theme-dark {
   --table-splitter-color: rgba(255,255,255,0.15);
   --table-zebra-background: rgba(255,255,255,0.05);
 
   --timing-blocked-color: rgba(235, 83, 104, 0.8);
   --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
   --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
   --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
@@ -78,246 +51,290 @@
   --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
   --timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */
   --timing-receive-color: rgba(112, 191, 83, 0.8); /* green */
 
   --sort-ascending-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
   --sort-descending-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
 }
 
-.request-list-container {
+.cropped-textbox .textbox-input {
+  /* workaround for textbox not supporting the @crop attribute */
+  text-overflow: ellipsis;
+}
+
+/* Toolbar */
+
+.devtools-toolbar-container {
   display: flex;
-  flex-direction: column;
-  width: 100%;
-  height: 100%;
+  justify-content: space-between;
 }
 
+.devtools-toolbar-group {
+  display: flex;
+  flex: 0 0 auto;
+  flex-wrap: nowrap;
+  align-items: center;
+}
+
+.theme-firebug .devtools-toolbar {
+  line-height: initial;
+}
+
+/* Empty requests list */
+
 .request-list-empty-notice {
   margin: 0;
   padding: 12px;
   font-size: 120%;
 }
 
-.notice-perf-message {
-  margin-top: 2px;
-}
-
 .requests-list-perf-notice-button {
   min-width: 30px;
-  min-height: 26px;
   margin: 0 5px;
   vertical-align: middle;
 }
 
 .requests-list-perf-notice-button::before {
   background-image: url(images/profiler-stopwatch.svg);
 }
 
 .requests-list-reload-notice-button {
   font-size: inherit;
   min-height: 26px;
   margin: 0 5px;
 }
 
-/* Network requests table */
+/* Requests list toolbar */
 
 .requests-list-toolbar {
   display: flex;
   padding: 0;
 }
 
 .requests-list-filter-buttons {
   display: flex;
   flex-wrap: nowrap;
 }
 
 .theme-firebug .requests-list-toolbar {
   height: 19px !important;
 }
 
-.requests-list-contents {
-  display: flex;
-  flex-direction: column;
-  overflow-x: hidden;
-  overflow-y: auto;
-  --timings-scale: 1;
-  --timings-rev-scale: 1;
-}
-
-.requests-list-subitem {
-  display: flex;
-  flex: none;
-  box-sizing: border-box;
-  align-items: center;
-  padding: 3px;
-  cursor: default;
-}
-
-.subitem-label {
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.requests-list-header {
-  display: flex;
-  flex: none;
-}
-
-.requests-list-header-button {
-  display: flex;
-  align-items: center;
-  flex: auto;
-  -moz-appearance: none;
+.requests-list-toolbar .requests-list-header-button {
   background-color: transparent;
   border-image: linear-gradient(transparent 15%,
                                 var(--theme-splitter-color) 15%,
                                 var(--theme-splitter-color) 85%,
                                 transparent 85%) 1 1;
   border-style: solid;
   border-width: 0;
   border-inline-start-width: 1px;
-  min-width: 1px;
-  min-height: 24px;
-  margin: 0;
-  padding-top: 2px;
-  padding-bottom: 2px;
-  padding-inline-start: 16px;
-  padding-inline-end: 0;
   text-align: center;
-  color: inherit;
-  font-weight: inherit !important;
+  height: 100%;
 }
 
-.requests-list-header-button::-moz-focus-inner {
-  border: 0;
-  padding: 0;
+/* Requests list */
+
+.requests-list-container {
+  flex: 1 1 auto;
+  overflow: hidden;
 }
 
-.requests-list-header:first-child .requests-list-header-button {
+/* Requests list headers */
+
+.requests-list-header-button.requests-list-status {
   border-width: 0;
 }
 
 .requests-list-header-button:hover {
   background-color: rgba(0, 0, 0, 0.1);
 }
 
 .requests-list-header-button > .button-text {
-  flex: auto;
+  display: inline-block;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
+  cursor: default;
+  max-width: 100%;
 }
 
 .requests-list-header-button > .button-icon {
-  flex: none;
-  height: 4px;
-  margin-inline-start: 3px;
-  margin-inline-end: 6px;
-  width: 7px;
-}
-
-.requests-list-header-button[data-sorted=ascending] > .button-icon {
-  background-image: var(--sort-ascending-image);
-}
-
-.requests-list-header-button[data-sorted=descending] > .button-icon {
-  background-image: var(--sort-descending-image);
+  display: inline-block;
+  height: 100%;
+  width: 0px;
 }
 
-.requests-list-waterfall-label-wrapper {
-  display: flex;
+.requests-list-header-button.ascending > .button-icon {
+  background: var(--sort-ascending-image) no-repeat center;
+  width: 7px;
+  margin-inline-start: 3px;
+  margin-inline-end: 6px;
 }
 
-.requests-list-header-button[data-sorted],
-.requests-list-header-button[data-sorted]:hover {
-  background-color: var(--theme-selection-background);
-  color: var(--theme-selection-color);
+.requests-list-header-button.descending > .button-icon {
+  background: var(--sort-descending-image) no-repeat center;
+  width: 7px;
+  margin-inline-start: 3px;
+  margin-inline-end: 6px;
 }
 
-.requests-list-header-button[data-sorted],
-.requests-list-header[data-active] + .requests-list-header .requests-list-header-button {
-  border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
-}
-
-/* Firebug theme support for Network panel header */
-
 .theme-firebug .requests-list-header {
   padding: 0 !important;
   font-weight: bold;
   background: linear-gradient(rgba(255, 255, 255, 0.05),
                               rgba(0, 0, 0, 0.05)),
                               #C8D2DC;
 }
 
-.theme-firebug .requests-list-header-button {
-  min-height: 17px;
-}
-
-.theme-firebug .requests-list-header-button > .button-icon {
-  height: 7px;
-}
-
-.theme-firebug .requests-list-header-button[data-sorted] {
-  background-color: #AAC3DC;
-}
-
-:root[platform="linux"].theme-firebug .requests-list-header-button[data-sorted] {
-  background-color: #FAC8AF !important;
-  color: inherit !important;
-}
-
 .theme-firebug .requests-list-header:hover:active {
   background-image: linear-gradient(rgba(0, 0, 0, 0.1),
                                     transparent);
 }
 
+/* Request list row and cell */
 
-/* Network requests table: specific column dimensions */
+.request-list-item {
+  display: flex;
+}
+
+.request-list-item.selected {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+}
 
-.requests-list-status {
-  max-width: 6em;
-  text-align: center;
-  width: 10vw;
+.request-list-item.fromCache > .requests-list-subitem:not(.requests-list-waterfall) {
+    opacity: 0.6;
+}
+
+.request-list-item:not(.selected).odd {
+  background-color: var(--table-zebra-background);
+}
+
+.request-list-item:not(.selected):hover {
+  background-color: var(--theme-selection-background-semitransparent);
+}
+
+.theme-firebug .request-list-item:not(.selected):hover {
+  background: #EFEFEF;
 }
 
-.requests-list-method,
-.requests-list-method-box {
-  max-width: 7em;
+.requests-list-subitem {
   text-align: center;
-  width: 10vw;
+  padding: 0 3px;
+  cursor: default;
+  margin: auto;
+  white-space: nowrap;
+}
+
+.theme-firebug .requests-list-subitem {
+  padding: 1px;
+}
+
+.theme-firebug .requests-list-status.requests-list-subitem {
+  font-weight: bold;
+}
+
+.subitem-label {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+/* Status column */
+
+.requests-list-status-icon {
+  display: inline-block;
+  height: 10px;
+  width: 10px;
+  margin-inline-start: 5px;
+  margin-inline-end: 5px;
+  border-radius: 10px;
+}
+
+.requests-list-status-icon:not([data-code]) {
+  background-color: var(--theme-content-color2);
 }
 
-.requests-list-icon-and-file {
-  width: 22vw;
+.requests-list-status-icon[data-code="cached"] {
+  border: 2px solid var(--theme-content-color2);
+  background-color: transparent;
+}
+
+.requests-list-status-icon[data-code^="1"] {
+  background-color: var(--theme-highlight-blue);
+}
+
+.requests-list-status-icon[data-code^="2"] {
+  background-color: var(--theme-highlight-green);
 }
 
-.requests-list-icon {
-  background: transparent;
-  width: 15px;
-  height: 15px;
-  margin-inline-end: 4px;
+.requests-list-status-icon[data-code^="3"] {
+  background-color: transparent;
+  width: 0;
+  height: 0;
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-bottom: 10px solid var(--theme-highlight-lightorange);
+  border-radius: 0;
+}
+
+.requests-list-status-icon[data-code^="4"] {
+  background-color: var(--theme-highlight-red);
+  border-radius: 0;
 }
 
-.requests-list-icon {
-  outline: 1px solid var(--table-splitter-color);
+.requests-list-status-icon[data-code^="5"] {
+  background-color: var(--theme-highlight-pink);
+  border-radius: 0;
+  transform: rotate(45deg);
+}
+
+.requests-list-status-code {
+  display: inline-block;
+  min-width: 20px;
+  vertical-align: sub;
+}
+
+/* Method column */
+
+.theme-firebug .requests-list-method.requests-list-subitem {
+  color: rgb(128, 128, 128);
 }
 
-.requests-list-security-and-domain {
-  width: 14vw;
+/* File column */
+
+.requests-list-file {
+  text-align: left;
 }
 
-.requests-security-state-icon {
-  flex: none;
+/* Domain column */
+
+.requests-list-domain {
+  text-align: left;
+}
+
+.requests-list-domain-url {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.requests-list-domain-icon {
+  display: inline-block;
   width: 16px;
   height: 16px;
   margin-inline-end: 4px;
+  background-repeat: no-repeat;
+  vertical-align: middle;
 }
 
-.request-list-item.selected .requests-security-state-icon {
+.request-list-item.selected .requests-list-domain-icon {
   filter: brightness(1.3);
 }
 
 .security-state-insecure {
   background-image: url(chrome://devtools/skin/images/security-state-insecure.svg);
 }
 
 .security-state-secure {
@@ -331,131 +348,63 @@
 .security-state-broken {
   background-image: url(chrome://devtools/skin/images/security-state-broken.svg);
 }
 
 .security-state-local {
   background-image: url(chrome://devtools/skin/images/globe.svg);
 }
 
-.requests-list-type,
-.requests-list-size {
-  max-width: 6em;
-  width: 8vw;
-  justify-content: center;
-}
-
-.requests-list-transferred {
-  max-width: 8em;
-  width: 8vw;
-  justify-content: center;
-}
+/* Cause column */
 
 .requests-list-cause {
-  max-width: 8em;
-  width: 8vw;
+  text-align: left;
 }
 
 .requests-list-cause-stack {
+  display: inline-block;
   background-color: var(--theme-body-color-alt);
   color: var(--theme-body-background);
   font-size: 8px;
   font-weight: bold;
-  line-height: 10px;
   border-radius: 3px;
   padding: 0 2px;
-  margin: 0;
   margin-inline-end: 3px;
-  -moz-user-select: none;
-}
-
-.request-list-item.selected .requests-list-transferred.theme-comment {
-  color: var(--theme-selection-color);
 }
 
-/* Network requests table: status codes */
-
-.requests-list-status-code {
-  margin-inline-start: 3px !important;
-  width: 3em;
-  margin-inline-end: -3em !important;
-}
+/* Waterfall column */
 
-.requests-list-status-icon {
-  background: #fff;
-  height: 10px;
-  width: 10px;
-  margin-inline-start: 5px;
-  margin-inline-end: 5px;
-  border-radius: 10px;
-  transition: box-shadow 0.5s ease-in-out;
-  box-sizing: border-box;
-}
-
-.request-list-item.selected .requests-list-status-icon {
-  filter: brightness(1.3);
-}
-
-.requests-list-status-icon:not([data-code]) {
-  background-color: var(--theme-content-color2);
+.requests-list-waterfall {
+  display: flex;
+  width: 100%;
+  overflow: hidden;
 }
 
-.requests-list-status-icon[data-code="cached"] {
-  border: 2px solid var(--theme-content-color2);
-  background-color: transparent;
-}
-
-.requests-list-status-icon[data-code^="1"] {
-  background-color: var(--theme-highlight-blue);
-}
-
-.requests-list-status-icon[data-code^="2"] {
-  background-color: var(--theme-highlight-green);
+.requests-list-timings-box {
+  display: inline-block;
+  height: 9px;
 }
 
-/* 3xx are triangles */
-.requests-list-status-icon[data-code^="3"] {
-  background-color: transparent;
-  width: 0;
-  height: 0;
-  border-left: 5px solid transparent;
-  border-right: 5px solid transparent;
-  border-bottom: 10px solid var(--theme-highlight-lightorange);
-  border-radius: 0;
+.theme-firebug .requests-list-timings-box {
+  background-image: linear-gradient(rgba(255, 255, 255, 0.3), rgba(0, 0, 0, 0.2));
+  height: 16px;
 }
 
-/* 4xx and 5xx are squares - error codes */
-.requests-list-status-icon[data-code^="4"] {
- background-color: var(--theme-highlight-red);
-  border-radius: 0; /* squares */
-}
-
-.requests-list-status-icon[data-code^="5"] {
-  background-color: var(--theme-highlight-pink);
-  border-radius: 0;
-  transform: rotate(45deg);
-}
-
-/* Network requests table: waterfall header */
-
-.requests-list-waterfall {
-  flex: auto;
-  padding-inline-start: 0;
-}
-
-.requests-list-waterfall-label-wrapper:not(.requests-list-waterfall-visible) {
-  padding-inline-start: 16px;
+.requests-list-timings {
+  display: flex;
+  flex: none;
+  align-items: center;
+  transform: scaleX(var(--timings-scale));
 }
 
 .requests-list-timings-division {
   padding-top: 2px;
   padding-inline-start: 4px;
   font-size: 75%;
   pointer-events: none;
-  box-sizing: border-box;
   text-align: start;
   /* Allow the timing label to shrink if the container gets too narrow.
    * The container width then is not limited by the content. */
   flex: initial;
 }
 
 .requests-list-timings-division:not(:first-child) {
   border-inline-start: 1px dashed;
@@ -477,39 +426,16 @@
   border-inline-start-color: #585959 !important;
 }
 
 .requests-list-timings-division[data-division-scale=second],
 .requests-list-timings-division[data-division-scale=minute] {
   font-weight: 600;
 }
 
-/* Network requests table: waterfall items */
-
-.requests-list-subitem.requests-list-waterfall {
-  padding-inline-start: 0;
-  padding-inline-end: 4px;
-  /* Background created on a <canvas> in js. */
-  /* @see devtools/client/netmonitor/netmonitor-view.js */
-  background-image: -moz-element(#waterfall-background);
-  background-repeat: repeat-y;
-  background-position: left center;
-}
-
-.requests-list-subitem.requests-list-waterfall:-moz-locale-dir(rtl) {
-  background-position: right center;
-}
-
-.requests-list-timings {
-  display: flex;
-  flex: none;
-  align-items: center;
-  transform: scaleX(var(--timings-scale));
-}
-
 .requests-list-timings:-moz-locale-dir(ltr) {
   transform-origin: left center;
 }
 
 .requests-list-timings:-moz-locale-dir(rtl) {
   transform-origin: right center;
 }
 
@@ -526,26 +452,16 @@
   padding-inline-start: 4px;
   font-size: 85%;
   font-weight: 600;
   white-space: nowrap;
   /* This node should not be scaled - apply a reversed transformation */
   transform: scaleX(var(--timings-rev-scale));
 }
 
-.requests-list-timings-box {
-  display: inline-block;
-  height: 9px;
-}
-
-.theme-firebug .requests-list-timings-box {
-  background-image: linear-gradient(rgba(255, 255, 255, 0.3), rgba(0, 0, 0, 0.2));
-  height: 16px;
-}
-
 .requests-list-timings-box.blocked {
   background-color: var(--timing-blocked-color);
 }
 
 .requests-list-timings-box.dns {
   background-color: var(--timing-dns-color);
 }
 
@@ -560,98 +476,33 @@
 .requests-list-timings-box.wait {
   background-color: var(--timing-wait-color);
 }
 
 .requests-list-timings-box.receive {
   background-color: var(--timing-receive-color);
 }
 
-/* SideMenuWidget */
-#network-table .request-list-empty-notice,
-#network-table .request-list-container {
-  background-color: var(--theme-body-background);
-}
-
-.request-list-item {
-  display: flex;
-  border-top-color: transparent;
-  border-bottom-color: transparent;
-  padding: 0;
-}
-
-.request-list-item.selected {
-  background-color: var(--theme-selection-background);
-  color: var(--theme-selection-color);
-}
-
-.request-list-item:not(.selected).odd {
-  background-color: var(--table-zebra-background);
-}
-
-.request-list-item:not(.selected):hover {
-  background-color: var(--theme-selection-background-semitransparent);
-}
-
-.request-list-item.fromCache > .requests-list-subitem:not(.requests-list-waterfall) {
-    opacity: 0.6;
-}
-
-.theme-firebug .request-list-item:not(.selected):hover {
-  background: #EFEFEF;
-}
-
-.theme-firebug .requests-list-subitem {
-  padding: 1px;
-}
-
-/* HTTP Status Column */
-.theme-firebug .requests-list-subitem.requests-list-status {
-  font-weight: bold;
-}
-
-/* Method Column */
-
-.theme-firebug .requests-list-subitem.requests-list-method-box {
-  color: rgb(128, 128, 128);
-}
-
-.request-list-item.selected .requests-list-method {
-  color: var(--theme-selection-color);
-}
-
-/* Size Column */
-.theme-firebug .requests-list-subitem.requests-list-size {
-  justify-content: end;
-  padding-inline-end: 4px;
-}
-
 /* Network details panel */
 
 .network-details-panel-toggle[disabled] {
   display: none;
 }
 
 .network-details-panel-toggle:-moz-locale-dir(ltr)::before,
 .network-details-panel-toggle.pane-collapsed:-moz-locale-dir(rtl)::before {
   background-image: var(--theme-pane-collapse-image);
 }
 
 .network-details-panel-toggle.pane-collapsed:-moz-locale-dir(ltr)::before,
 .network-details-panel-toggle:-moz-locale-dir(rtl)::before {
   background-image: var(--theme-pane-expand-image);
 }
 
-/* Network request details tabpanels */
-
-.theme-firebug .variables-view-scope:focus > .title {
-  color: var(--theme-body-color);
-}
-
-/* Summary tabpanel */
+/* Summary panel */
 
 .tabpanel-summary-container {
   padding: 1px;
 }
 
 .tabpanel-summary-label {
   display: inline-block;
   padding-inline-start: 4px;
@@ -663,17 +514,17 @@
   color: inherit;
   padding-inline-start: 3px;
 }
 
 .theme-dark .tabpanel-summary-value {
   color: var(--theme-selection-color);
 }
 
-/* Headers tabpanel */
+/* Headers panel */
 
 .headers-overview {
   background: var(--theme-toolbar-background);
 }
 
 .headers-summary .status-text {
     width: auto!important;
 }
@@ -685,17 +536,17 @@
   white-space: nowrap;
   flex-grow: 1;
 }
 
 .headers-summary .learn-more-link:hover {
   text-decoration: underline;
 }
 
-/* Response tabpanel */
+/* Response panel */
 
 .response-error-header {
   margin: 0;
   padding: 3px 8px;
   background-color: var(--theme-highlight-red);
   color: var(--theme-selection-color);
 }
 
@@ -711,17 +562,17 @@
 .response-image {
   background: #fff;
   border: 1px dashed GrayText;
   margin-bottom: 10px;
   max-width: 300px;
   max-height: 100px;
 }
 
-/* Timings tabpanel */
+/* Timings panel */
 
 .timings-container {
   display: flex;
 }
 
 .timings-label {
   width: 10em;
 }
@@ -741,17 +592,17 @@
   min-width: 1px;
   transition: width 0.2s ease-out;
 }
 
 .theme-firebug .requests-list-timings-total {
   color: var(--theme-body-color);
 }
 
-/* Security tabpanel */
+/* Security panel */
 
 /* Overwrite tree-view cell colon `:` for security panel and tree section */
 .security-panel .treeTable .treeLabelCell::after,
 .treeTable .tree-section .treeLabelCell::after {
   content: "";
 }
 
 /* Layout additional warning icon in tree value cell  */
@@ -769,17 +620,17 @@
 }
 
 @media (min-resolution: 1.1dppx) {
   .security-warning-icon {
     background-image: url(images/alerticon-warning@2x.png);
   }
 }
 
-/* Custom request view */
+/* Custom request panel */
 
 .custom-request-panel {
   height: 100%;
   overflow: auto;
   padding: 0 4px;
   background-color: var(--theme-sidebar-background);
 }
 
@@ -818,17 +669,17 @@
   width: 4.5em;
 }
 
 .custom-url-value {
   flex-grow: 1;
   margin-inline-start: 6px;
 }
 
-/* Performance analysis buttons */
+/* Statistics summary button */
 
 .requests-list-network-summary-button {
   display: flex;
   flex-wrap: nowrap;
   align-items: center;
   background: none;
   box-shadow: none;
   border-color: transparent;
@@ -851,17 +702,17 @@
   margin-inline-start: 0.5em;
 }
 
 .requests-list-network-summary-button:hover > .summary-info-icon,
 .requests-list-network-summary-button:hover > .summary-info-text {
   opacity: 1;
 }
 
-/* Performance analysis view */
+/* Statistics panel */
 
 .statistics-panel {
   display: flex;
   height: 100vh;
 }
 
 .statistics-panel .devtools-toolbarbutton.back-button {
   min-width: 4em;
@@ -1023,103 +874,48 @@
 
 .theme-firebug .chart-colored-blob[name=flash] {
   fill: rgba(84, 235, 159, 0.8); /* cyan */
   background: rgba(84, 235, 159, 0.8);
 }
 
 /* Responsive sidebar */
 @media (max-width: 700px) {
-  #toolbar-spacer,
   .network-details-panel-toggle,
-  .requests-list-network-summary-button > .summary-info-text {
-    display: none;
-  }
-
-  .requests-list-toolbar {
-    height: 22px;
-  }
-
-  .requests-list-header-button {
-    min-height: 22px;
-    padding-left: 8px;
-  }
-
-  .requests-list-status {
-    max-width: none;
-    width: 10vw;
-  }
-
-  .requests-list-status-code {
-    width: auto;
-  }
-
-  .requests-list-method,
-  .requests-list-method-box {
-    max-width: none;
-    width: 12vw;
-  }
-
-  .requests-list-icon-and-file {
-    width: 22vw;
-  }
-
-  .requests-list-security-and-domain {
-    width: 16vw;
-  }
-
-  .requests-list-cause,
-  .requests-list-type,
-  .requests-list-transferred,
-  .requests-list-size {
-    max-width: none;
-    width: 10vw;
-  }
-
+  .requests-list-network-summary-button > .summary-info-text,
   .requests-list-waterfall {
     display: none;
   }
 
   .statistics-panel .charts-container {
     flex-direction: column;
     /* Minus 4em for statistics back button width */
     width: calc(100% - 4em);
   }
 
   .statistics-panel .splitter {
     width: 100%;
     height: 1px;
   }
+
+  :root[platform="linux"] .requests-list-header-button {
+    font-size: 85%;
+  }
 }
 
 /* Platform overrides (copied in from the old platform specific files) */
 :root[platform="win"] .requests-list-header-button > .button-box {
   padding: 0;
 }
 
 :root[platform="win"] .requests-list-timings-division {
   padding-top: 1px;
   font-size: 90%;
 }
 
-:root[platform="linux"] #headers-summary-resend {
-  padding: 4px;
-}
-
-:root[platform="linux"] #toggle-raw-headers {
-  padding: 4px;
-}
-
-/* Responsive sidebar */
-@media (max-width: 700px) {
-  :root[platform="linux"] .requests-list-header-button {
-    font-size: 85%;
-  }
-}
-
 .textbox-input {
   text-overflow: ellipsis;
   border: none;
   background: none;
   color: inherit;
   width: 100%;
 }
 
@@ -1214,17 +1010,17 @@
 }
 
 .tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel,
 .tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel:hover {
   color: var(--theme-body-color-alt);
 }
 
 .tree-container .treeTable .treeValueCell {
-  /* FIXME: Make value cell can be reduced to shorter width */
+  /* Make value cell can be reduced to shorter width */
   max-width: 0;
   padding-inline-end: 5px;
 }
 
 .headers-summary input:not([type="button"]) {
   width: 100%;
   background: none;
   border: none;
@@ -1267,17 +1063,16 @@
   padding: 0 4px;
 }
 
 .headers-summary .raw-headers textarea {
   width: 100%;
   height: 50vh;
   font: message-box;
   resize: none;
-  box-sizing: border-box;
 }
 
 .headers-summary .raw-headers .tabpanel-summary-label {
   padding: 0 0 4px 0;
 }
 
 .empty-notice {
   color: var(--theme-body-color-alt);