Repository: elastic/kibana
Pull request #212478: [Performance][Security Solution][2/4] - Timeline Performance
Author: login: michaelolo24
Labels: name: release_note:skip
description: Skip the PR/issue when compiling release notes
name: Team:Threat Hunting:Investigations
description: Security Solution Threat Hunting Investigations Team
name: ci:cloud-deploy
description: Create or update a Cloud deployment
name: ci:project-redeploy
description: Always create a new Cloud project
name: backport:version
description: Backport to applied version labels
name: v9.1.0
name: v8.19.0
name: v8.17.5
name: v9.0.3
name: v8.18.3
Created: 2025-02-26T08:43:26Z | Merged: 2025-03-11T16:56:45Z

## Summary
Part 2 of https://github.com/elastic/kibana/pull/212173

### Testing
For setup see testing section here: https://github.com/elastic/kibana/pull/212173#issue-2870522020

**Areas/How to test:**
- For the following pages, test there are no `fields` api requests in the inspector network tab when visiting from another page. IF YOU REFRESH on any of these pages, you will see these requests as they are called by the Query Search Bar and the `useInitSourcerer` call
  - Cases Page
  - Dashboard Page
  - Timelines Page
- Timeline
  - All Tabs
    - Does it show the loading screen on first interaction?
    - Does the `fields` api fire on first interaction with the tab
    - When you navigate back to those tabs, do they not re-render?
- All other pages hosting timeline
 - Do you feel like the performance is generally better?


### Background

When investigating the performance of the security solution application, one of the issues that was observed was queries to the `fields` api on pages that had no reason making that request (such as Cases, or the Dashboards list view). This was due to the background background loaded tabs of timeline loading the relevant `dataView` necessary for their search functionality. When the fields request is significantly large this can have a massive impact on the experience of users on pages that should be relatively responsive. 

To fix this a few changes were made. 

1. First the `withDataView` HOC was removed as it was only used in 2 components that shared a parent - child relationship, and the child `UnifiedTimeline` was only used in the parent. The hook that HOC calls was not caching the dataView being created, so `dataView.create` was being called up to 6 times unnecessarily. Now it is only called once in each tab.

2. A new wrapper `OnDemandRenderer` (open to different naming 😅) was created that will not render any of the nested tabs until they are opened. Once they are opened, they stay in memory, to avoid re-calling expensive api's every time a user switches tabs.
    _Note_: There is currently a known issue where navigating between various routes in security solution causes the whole application to unmount and re-mount. Which means every page change will lead to timeline needing to be re-loaded when the tab is opened. This is being resolved in a separate effort.

3. Additional checks were added to the `useTimelineEvents` hook to limit additional re-renders caused by unnecessary reference changes when the underlying values never actually change

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
### Identify risks





<!--ONMERGE {"backportTargets":["8.17","8.x"]} ONMERGE-->


Diff:
diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/drag_and_drop/cell_actions_wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/drag_and_drop/cell_actions_wrapper.tsx
index d10cd5aaf1984..7d20a645abc35 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/common/components/drag_and_drop/cell_actions_wrapper.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/drag_and_drop/cell_actions_wrapper.tsx
@@ -16,7 +16,7 @@ import {
   SecurityCellActionType,
 } from '../cell_actions';
 import { getSourcererScopeId } from '../../../helpers';
-import { TimelineContext } from '../../../timelines/components/timeline';
+import { TimelineContext } from '../../../timelines/components/timeline/context';
 
 import { TableContext } from '../events_viewer/shared';
 
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx
index fa71f93dccb99..9e8453fe08047 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx
@@ -15,7 +15,7 @@ import { AddTimelineButton } from './add_timeline_button';
 import { timelineActions } from '../../store';
 import { TimelineSaveStatus } from '../save_status';
 import { AddToFavoritesButton } from '../add_to_favorites';
-import { TimelineEventsCountBadge } from '../../../common/hooks/use_timeline_events_count';
+import TimelineQueryTabEventsCount from '../timeline/tabs/query/events_count';
 
 interface TimelineBottomBarProps {
   /**
@@ -63,9 +63,9 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(
               {title}
             </EuiLink>
           </EuiFlexItem>
-          {!show && ( // this is a hack because TimelineEventsCountBadge is using react-reverse-portal so the component which is used in multiple places cannot be visible in multiple places at the same time
+          {!show && ( // We only want to show this when the timeline modal is closed
             <EuiFlexItem grow={false} data-test-subj="timeline-event-count-badge">
-              <TimelineEventsCountBadge />
+              <TimelineQueryTabEventsCount timelineId={timelineId} />
             </EuiFlexItem>
           )}
           <EuiFlexItem grow={false}>
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/more_container/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/more_container/index.tsx
index 8936b77d22fb5..25c5cdcf12be1 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/more_container/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/field_renderers/more_container/index.tsx
@@ -9,7 +9,7 @@ import React, { useContext, useMemo } from 'react';
 import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
 import { css } from '@emotion/css';
 import classNames from 'classnames';
-import { TimelineContext } from '../../timeline';
+import { TimelineContext } from '../../timeline/context';
 import { getSourcererScopeId } from '../../../../helpers';
 import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers';
 import { defaultToEmptyTag } from '../../../../common/components/empty_value';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx
index 77f26075581e0..7244fe7630d86 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx
@@ -14,6 +14,9 @@ import type { UnifiedTimelineBodyProps } from './unified_timeline_body';
 import { UnifiedTimelineBody } from './unified_timeline_body';
 import { render } from '@testing-library/react';
 import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock';
+import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
+import { mockSourcererScope } from '../../../../sourcerer/containers/mocks';
+import { DataView } from '@kbn/data-views-plugin/common';
 
 jest.mock('../unified_components', () => {
   return {
@@ -21,6 +24,18 @@ jest.mock('../unified_components', () => {
   };
 });
 
+const mockDataView = new DataView({
+  spec: mockSourcererScope.sourcererDataView,
+  fieldFormats: fieldFormatsMock,
+});
+
+// Not returning an actual dataView here, just an object as a non-null value;
+const mockUseGetScopedSourcererDataView = jest.fn().mockImplementation(() => mockDataView);
+
+jest.mock('../../../../sourcerer/components/use_get_sourcerer_data_view', () => ({
+  useGetScopedSourcererDataView: () => mockUseGetScopedSourcererDataView(),
+}));
+
 const mockEventsData = structuredClone(mockTimelineData);
 
 const defaultProps: UnifiedTimelineBodyProps = {
@@ -77,4 +92,11 @@ describe('UnifiedTimelineBody', () => {
       {}
     );
   });
+
+  it('should render the dataview error component when no dataView is provided', () => {
+    mockUseGetScopedSourcererDataView.mockImplementationOnce(() => undefined);
+    const { queryByTestId } = renderTestComponents();
+
+    expect(queryByTestId('dataViewErrorComponent')).toBeInTheDocument();
+  });
 });
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx
index b705717fc437f..41f9402865ed1 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx
@@ -8,11 +8,15 @@
 import type { ComponentProps, ReactElement } from 'react';
 import React, { useMemo } from 'react';
 import { RootDragDropProvider } from '@kbn/dom-drag-drop';
+import { useGetScopedSourcererDataView } from '../../../../sourcerer/components/use_get_sourcerer_data_view';
+import { DataViewErrorComponent } from '../../../../common/components/with_data_view/data_view_error';
 import { StyledTableFlexGroup, StyledUnifiedTableFlexItem } from '../unified_components/styles';
 import { UnifiedTimeline } from '../unified_components';
 import { defaultUdtHeaders } from './column_headers/default_headers';
+import { SourcererScopeName } from '../../../../sourcerer/store/model';
 
-export interface UnifiedTimelineBodyProps extends ComponentProps<typeof UnifiedTimeline> {
+export interface UnifiedTimelineBodyProps
+  extends Omit<ComponentProps<typeof UnifiedTimeline>, 'dataView'> {
   header: ReactElement;
 }
 
@@ -37,7 +41,9 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => {
     leadingControlColumns,
     onUpdatePageIndex,
   } = props;
-
+  const dataView = useGetScopedSourcererDataView({
+    sourcererScope: SourcererScopeName.timeline,
+  });
   const columnsHeader = useMemo(() => columns ?? defaultUdtHeaders, [columns]);
 
   return (
@@ -48,26 +54,31 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => {
         data-test-subj="unifiedTimelineBody"
       >
         <RootDragDropProvider>
-          <UnifiedTimeline
-            columns={columnsHeader}
-            rowRenderers={rowRenderers}
-            isSortEnabled={isSortEnabled}
-            timelineId={timelineId}
-            itemsPerPage={itemsPerPage}
-            itemsPerPageOptions={itemsPerPageOptions}
-            sort={sort}
-            events={events}
-            refetch={refetch}
-            dataLoadingState={dataLoadingState}
-            totalCount={totalCount}
-            onFetchMoreRecords={onFetchMoreRecords}
-            activeTab={activeTab}
-            updatedAt={updatedAt}
-            isTextBasedQuery={false}
-            trailingControlColumns={trailingControlColumns}
-            leadingControlColumns={leadingControlColumns}
-            onUpdatePageIndex={onUpdatePageIndex}
-          />
+          {dataView ? (
+            <UnifiedTimeline
+              columns={columnsHeader}
+              dataView={dataView}
+              rowRenderers={rowRenderers}
+              isSortEnabled={isSortEnabled}
+              timelineId={timelineId}
+              itemsPerPage={itemsPerPage}
+              itemsPerPageOptions={itemsPerPageOptions}
+              sort={sort}
+              events={events}
+              refetch={refetch}
+              dataLoadingState={dataLoadingState}
+              totalCount={totalCount}
+              onFetchMoreRecords={onFetchMoreRecords}
+              activeTab={activeTab}
+              updatedAt={updatedAt}
+              isTextBasedQuery={false}
+              trailingControlColumns={trailingControlColumns}
+              leadingControlColumns={leadingControlColumns}
+              onUpdatePageIndex={onUpdatePageIndex}
+            />
+          ) : (
+            <DataViewErrorComponent />
+          )}
         </RootDragDropProvider>
       </StyledUnifiedTableFlexItem>
     </StyledTableFlexGroup>
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/context.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/context.ts
new file mode 100644
index 0000000000000..eebd271eec3af
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/context.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { createContext } from 'react';
+
+export const TimelineContext = createContext<{
+  timelineId: string | null;
+}>({ timelineId: null });
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx
index 930c09fd48bb5..8e35ca9a1a4ed 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx
@@ -7,7 +7,7 @@
 
 import { pick } from 'lodash/fp';
 import { EuiPanel, EuiProgress, EuiText } from '@elastic/eui';
-import React, { useCallback, useEffect, useMemo, useRef, createContext } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
 import styled from 'styled-components';
 
@@ -30,6 +30,7 @@ import { EXIT_FULL_SCREEN_CLASS_NAME } from '../../../common/components/exit_ful
 import { useResolveConflict } from '../../../common/hooks/use_resolve_conflict';
 import { sourcererSelectors } from '../../../common/store';
 import { defaultUdtHeaders } from './body/column_headers/default_headers';
+import { TimelineContext } from './context';
 
 const TimelineBody = styled.div`
   height: 100%;
@@ -37,7 +38,6 @@ const TimelineBody = styled.div`
   flex-direction: column;
 `;
 
-export const TimelineContext = createContext<{ timelineId: string | null }>({ timelineId: null });
 export interface Props {
   renderCellValue: (props: CellValueElementProps) => React.ReactNode;
   rowRenderers: RowRenderer[];
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx
index 88b134cf00e30..5a2ffe6e8c7af 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx
@@ -45,17 +45,7 @@ import { selectTimelineById, selectTimelineESQLSavedSearchId } from '../../../st
 import { fetchNotesBySavedObjectIds, selectSortedNotesBySavedObjectId } from '../../../../notes';
 import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants';
 import { useUserPrivileges } from '../../../../common/components/user_privileges';
-
-const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>(
-  ({ $isVisible = false, isOverflowYScroll = false }) => ({
-    style: {
-      display: $isVisible ? 'flex' : 'none',
-      overflow: isOverflowYScroll ? 'hidden scroll' : 'hidden',
-    },
-  })
-)<{ $isVisible: boolean; isOverflowYScroll?: boolean }>`
-  flex: 1;
-`;
+import { LazyTimelineTabRenderer, TimelineTabFallback } from './lazy_timeline_tab_renderer';
 
 /**
  * A HOC which supplies React.Suspense with a fallback component
@@ -76,13 +66,35 @@ const tabWithSuspense = <P extends {}, R = {}>(
   return Comp;
 };
 
-const QueryTab = tabWithSuspense(lazy(() => import('./query')));
-const EqlTab = tabWithSuspense(lazy(() => import('./eql')));
-const GraphTab = tabWithSuspense(lazy(() => import('./graph')));
-const NotesTab = tabWithSuspense(lazy(() => import('./notes')));
-const PinnedTab = tabWithSuspense(lazy(() => import('./pinned')));
-const SessionTab = tabWithSuspense(lazy(() => import('./session')));
-const EsqlTab = tabWithSuspense(lazy(() => import('./esql')));
+const QueryTab = tabWithSuspense(
+  lazy(() => import('./query')),
+  <TimelineTabFallback />
+);
+const EqlTab = tabWithSuspense(
+  lazy(() => import('./eql')),
+  <TimelineTabFallback />
+);
+const GraphTab = tabWithSuspense(
+  lazy(() => import('./graph')),
+  <TimelineTabFallback />
+);
+const NotesTab = tabWithSuspense(
+  lazy(() => import('./notes')),
+  <TimelineTabFallback />
+);
+const PinnedTab = tabWithSuspense(
+  lazy(() => import('./pinned')),
+  <TimelineTabFallback />
+);
+const SessionTab = tabWithSuspense(
+  lazy(() => import('./session')),
+  <TimelineTabFallback />
+);
+const EsqlTab = tabWithSuspense(
+  lazy(() => import('./esql')),
+  <TimelineTabFallback />
+);
+
 interface BasicTimelineTab {
   renderCellValue: (props: CellValueElementProps) => React.ReactNode;
   rowRenderers: RowRenderer[];
@@ -138,60 +150,60 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
       [activeTimelineTab]
     );
 
-    /* Future developer -> why are we doing that
-     * It is really expansive to re-render the QueryTab because the drag/drop
-     * Therefore, we are only hiding its dom when switching to another tab
-     * to avoid mounting/un-mounting === re-render
-     */
     return (
       <>
-        <HideShowContainer
-          $isVisible={TimelineTabs.query === activeTimelineTab}
-          data-test-subj={`timeline-tab-content-${TimelineTabs.query}`}
+        <LazyTimelineTabRenderer
+          timelineId={timelineId}
+          shouldShowTab={TimelineTabs.query === activeTimelineTab}
+          dataTestSubj={`timeline-tab-content-${TimelineTabs.query}`}
         >
           <QueryTab
             renderCellValue={renderCellValue}
             rowRenderers={rowRenderers}
             timelineId={timelineId}
           />
-        </HideShowContainer>
+        </LazyTimelineTabRenderer>
         {showTimeline && shouldShowESQLTab && activeTimelineTab === TimelineTabs.esql && (
-          <HideShowContainer
-            $isVisible={true}
-            data-test-subj={`timeline-tab-content-${TimelineTabs.esql}`}
+          <LazyTimelineTabRenderer
+            timelineId={timelineId}
+            shouldShowTab={true}
+            dataTestSubj={`timeline-tab-content-${TimelineTabs.esql}`}
           >
             <EsqlTab timelineId={timelineId} />
-          </HideShowContainer>
+          </LazyTimelineTabRenderer>
         )}
-        <HideShowContainer
-          $isVisible={TimelineTabs.pinned === activeTimelineTab}
-          data-test-subj={`timeline-tab-content-${TimelineTabs.pinned}`}
+        <LazyTimelineTabRenderer
+          timelineId={timelineId}
+          shouldShowTab={TimelineTabs.pinned === activeTimelineTab}
+          dataTestSubj={`timeline-tab-content-${TimelineTabs.pinned}`}
         >
           <PinnedTab
             renderCellValue={renderCellValue}
             rowRenderers={rowRenderers}
             timelineId={timelineId}
           />
-        </HideShowContainer>
+        </LazyTimelineTabRenderer>
         {timelineType === TimelineTypeEnum.default && (
-          <HideShowContainer
-            $isVisible={TimelineTabs.eql === activeTimelineTab}
-            data-test-subj={`timeline-tab-content-${TimelineTabs.eql}`}
+          <LazyTimelineTabRenderer
+            timelineId={timelineId}
+            shouldShowTab={TimelineTabs.eql === activeTimelineTab}
+            dataTestSubj={`timeline-tab-content-${TimelineTabs.eql}`}
           >
             <EqlTab
               renderCellValue={renderCellValue}
               rowRenderers={rowRenderers}
               timelineId={timelineId}
             />
-          </HideShowContainer>
+          </LazyTimelineTabRenderer>
         )}
-        <HideShowContainer
-          $isVisible={isGraphOrNotesTabs}
+        <LazyTimelineTabRenderer
+          timelineId={timelineId}
+          shouldShowTab={isGraphOrNotesTabs}
           isOverflowYScroll={activeTimelineTab === TimelineTabs.session}
-          data-test-subj={`timeline-tab-content-${TimelineTabs.graph}-${TimelineTabs.notes}`}
+          dataTestSubj={`timeline-tab-content-${TimelineTabs.graph}-${TimelineTabs.notes}`}
         >
-          {isGraphOrNotesTabs && getTab(activeTimelineTab)}
-        </HideShowContainer>
+          {isGraphOrNotesTabs ? getTab(activeTimelineTab) : null}
+        </LazyTimelineTabRenderer>
       </>
     );
   }
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/lazy_timeline_tab_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/lazy_timeline_tab_renderer.test.tsx
new file mode 100644
index 0000000000000..adda3b2043223
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/lazy_timeline_tab_renderer.test.tsx
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect } from 'react';
+import { render } from '@testing-library/react';
+import type { LazyTimelineTabRendererProps } from './lazy_timeline_tab_renderer';
+import { LazyTimelineTabRenderer } from './lazy_timeline_tab_renderer';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { TimelineId } from '../../../../../common/types';
+
+jest.mock('../../../../common/hooks/use_selector');
+
+describe('LazyTimelineTabRenderer', () => {
+  const mockUseDeepEqualSelector = useDeepEqualSelector as jest.Mock;
+  const defaultProps = {
+    dataTestSubj: 'test',
+    shouldShowTab: true,
+    isOverflowYScroll: false,
+    timelineId: TimelineId.test,
+  };
+
+  const TestComponent = ({ children, ...restProps }: Partial<LazyTimelineTabRendererProps>) => (
+    <LazyTimelineTabRenderer {...defaultProps} {...restProps}>
+      <div>{children ?? 'test component'}</div>
+    </LazyTimelineTabRenderer>
+  );
+  const renderTestComponents = (props?: Partial<LazyTimelineTabRendererProps>) => {
+    const { children, ...restProps } = props ?? {};
+    return render(<TestComponent {...restProps}>{children}</TestComponent>);
+  };
+
+  beforeEach(() => {
+    mockUseDeepEqualSelector.mockClear();
+  });
+
+  describe('timeline visibility', () => {
+    it('should NOT render children when the timeline show status is false', () => {
+      mockUseDeepEqualSelector.mockReturnValue({ show: false });
+      const { queryByText } = renderTestComponents();
+      expect(queryByText('test component')).not.toBeInTheDocument();
+    });
+
+    it('should render children when the timeline show status is true', () => {
+      mockUseDeepEqualSelector.mockReturnValue({ show: true });
+
+      const { getByText } = renderTestComponents();
+
+      expect(getByText('test component')).toBeInTheDocument();
+    });
+  });
+
+  describe('tab visibility', () => {
+    it('should not render children when show tab is false', () => {
+      const { queryByText } = renderTestComponents({ shouldShowTab: false });
+
+      expect(queryByText('test component')).not.toBeInTheDocument();
+    });
+  });
+
+  describe('re-rendering', () => {
+    const testChildString = 'new content';
+    const mockFnShouldThatShouldOnlyRunOnce = jest.fn();
+
+    const TestChild = () => {
+      useEffect(() => {
+        mockFnShouldThatShouldOnlyRunOnce();
+      }, []);
+      return <div>{testChildString}</div>;
+    };
+
+    const RerenderTestComponent = (props?: Partial<LazyTimelineTabRendererProps>) => (
+      <TestComponent {...props}>
+        <TestChild />
+      </TestComponent>
+    );
+
+    beforeEach(() => {
+      jest.resetAllMocks();
+      mockUseDeepEqualSelector.mockReturnValue({ show: true });
+    });
+
+    it('should NOT re-render children after the first render', () => {
+      const { queryByText } = render(<RerenderTestComponent />);
+      expect(queryByText(testChildString)).toBeInTheDocument();
+      expect(mockFnShouldThatShouldOnlyRunOnce).toHaveBeenCalledTimes(1);
+    });
+
+    it('should NOT re-render children even if timeline show status changes', () => {
+      const { rerender, queryByText } = render(<RerenderTestComponent />);
+      mockUseDeepEqualSelector.mockReturnValue({ show: false });
+      rerender(<RerenderTestComponent />);
+      expect(queryByText(testChildString)).toBeInTheDocument();
+      expect(mockFnShouldThatShouldOnlyRunOnce).toHaveBeenCalledTimes(1);
+    });
+
+    it('should NOT re-render children even if tab visibility status changes', () => {
+      const { rerender, queryByText } = render(<RerenderTestComponent />);
+      rerender(<RerenderTestComponent shouldShowTab={false} />);
+      rerender(<RerenderTestComponent shouldShowTab={true} />);
+      expect(queryByText(testChildString)).toBeInTheDocument();
+      expect(mockFnShouldThatShouldOnlyRunOnce).toHaveBeenCalledTimes(1);
+    });
+
+    it('should re-render if the component is unmounted and remounted', () => {
+      const { rerender, queryByText, unmount } = render(<RerenderTestComponent />);
+      unmount();
+      rerender(<RerenderTestComponent shouldShowTab={true} />);
+      expect(queryByText(testChildString)).toBeInTheDocument();
+      expect(mockFnShouldThatShouldOnlyRunOnce).toHaveBeenCalledTimes(2);
+    });
+  });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/lazy_timeline_tab_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/lazy_timeline_tab_renderer.tsx
new file mode 100644
index 0000000000000..31c3bb52b7cdd
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/lazy_timeline_tab_renderer.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
+import type { TimelineId } from '../../../../../common/types';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
+
+export interface LazyTimelineTabRendererProps {
+  children: React.ReactElement | null;
+  dataTestSubj: string;
+  isOverflowYScroll?: boolean;
+  shouldShowTab: boolean;
+  timelineId: TimelineId;
+}
+
+/**
+ * We check for the timeline open status to request the fields for the fields browser. The fields request
+ * is often a much longer running request for customers with a significant number of indices and fields in those indices.
+ * This request should only be made after the user has decided to interact with a specific tab in the timeline to prevent any performance impacts
+ * to the underlying security solution views, as this query will always run when the timeline exists on the page.
+ *
+ * `hasTimelineTabBeenOpenedOnce` - We want to keep timeline loading times as fast as possible after the user
+ * has chosen to interact with timeline at least once, so we use this flag to prevent re-requesting of this fields data
+ * every time timeline is closed and re-opened after the first interaction.
+ */
+export const LazyTimelineTabRenderer = React.memo(
+  ({
+    children,
+    dataTestSubj,
+    shouldShowTab,
+    isOverflowYScroll,
+    timelineId,
+  }: LazyTimelineTabRendererProps) => {
+    const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
+    const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId));
+
+    const [hasTimelineTabBeenOpenedOnce, setHasTimelineTabBeenOpenedOnce] = useState(false);
+
+    useEffect(() => {
+      if (!hasTimelineTabBeenOpenedOnce && show && shouldShowTab) {
+        setHasTimelineTabBeenOpenedOnce(true);
+      }
+    }, [hasTimelineTabBeenOpenedOnce, shouldShowTab, show]);
+
+    return (
+      <div
+        // The shouldShowTab check here is necessary for the flex container to accurately size to the modal window when it's opened
+        css={css`
+          display: ${shouldShowTab ? 'flex' : 'none'};
+          overflow: ${isOverflowYScroll ? 'hidden scroll' : 'hidden'};
+          flex: 1;
+        `}
+        data-test-subj={dataTestSubj}
+      >
+        {hasTimelineTabBeenOpenedOnce ? children : <TimelineTabFallback />}
+      </div>
+    );
+  }
+);
+
+LazyTimelineTabRenderer.displayName = 'LazyTimelineTabRenderer';
+
+export const TimelineTabFallback = () => (
+  <EuiFlexGroup direction="row" justifyContent="spaceAround">
+    <EuiFlexItem
+      grow={false}
+      css={css`
+        justify-content: center;
+      `}
+    >
+      <EuiLoadingElastic size="xxl" />
+    </EuiFlexItem>
+  </EuiFlexGroup>
+);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/events_count.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/events_count.tsx
new file mode 100644
index 0000000000000..621a1c79d9660
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/events_count.tsx
@@ -0,0 +1,227 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isEmpty } from 'lodash/fp';
+import React, { useEffect, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
+import { getEsQueryConfig } from '@kbn/data-plugin/common';
+import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import { DataLoadingState } from '@kbn/unified-data-table';
+import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
+import { useTimelineDataFilters } from '../../../../containers/use_timeline_data_filters';
+import { useInvalidFilterQuery } from '../../../../../common/hooks/use_invalid_filter_query';
+import { timelineActions, timelineSelectors } from '../../../../store';
+import type { Direction } from '../../../../../../common/search_strategy';
+import { useTimelineEvents } from '../../../../containers';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { combineQueries } from '../../../../../common/lib/kuery';
+import type {
+  KueryFilterQuery,
+  KueryFilterQueryKind,
+} from '../../../../../../common/types/timeline';
+import type { inputsModel } from '../../../../../common/store';
+import { inputsSelectors } from '../../../../../common/store';
+import { SourcererScopeName } from '../../../../../sourcerer/store/model';
+import { timelineDefaults } from '../../../../store/defaults';
+import { useSourcererDataView } from '../../../../../sourcerer/containers';
+import { isActiveTimeline } from '../../../../../helpers';
+import type { TimelineModel } from '../../../../store/model';
+import { useTimelineColumns } from '../shared/use_timeline_columns';
+import { EventsCountBadge } from '../shared/layout';
+
+/**
+ * TODO: This component is a pared down duplicate of the logic used in timeline/tabs/query/index.tsx
+ * This is only done to support the events count badge that shows in the bottom bar of the application,
+ * without needing to render the entire query tab, which is expensive to render at a significant enough fields count.
+ * The long term solution is a centralized query either via RTK or useQuery, that both can read from, but that is out of scope
+ * at this current time.
+ */
+
+const emptyFieldsList: string[] = [];
+export const TimelineQueryTabEventsCountComponent: React.FC<{ timelineId: string }> = ({
+  timelineId,
+}) => {
+  const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+  const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterKuerySelector(), []);
+  const getInputsTimeline = useMemo(() => inputsSelectors.getTimelineSelector(), []);
+
+  const timeline: TimelineModel = useDeepEqualSelector(
+    (state) => getTimeline(state, timelineId) ?? timelineDefaults
+  );
+  const input: inputsModel.InputsRange = useDeepEqualSelector((state) => getInputsTimeline(state));
+  const { timerange: { to: end, from: start, kind: timerangeKind } = {} } = input;
+  const {
+    columns,
+    dataProviders,
+    filters: currentTimelineFilters,
+    kqlMode,
+    sort,
+    timelineType,
+  } = timeline;
+
+  const kqlQueryTimeline: KueryFilterQuery | null = useDeepEqualSelector((state) =>
+    getKqlQueryTimeline(state, timelineId)
+  );
+  const filters = useMemo(
+    () => (kqlMode === 'filter' ? currentTimelineFilters || [] : []),
+    [currentTimelineFilters, kqlMode]
+  );
+
+  // return events on empty search
+  const kqlQueryExpression =
+    isEmpty(dataProviders) &&
+    isEmpty(kqlQueryTimeline?.expression ?? '') &&
+    timelineType === 'template'
+      ? ' '
+      : kqlQueryTimeline?.expression ?? '';
+
+  const kqlQueryLanguage =
+    isEmpty(dataProviders) && timelineType === 'template'
+      ? 'kuery'
+      : kqlQueryTimeline?.kind ?? 'kuery';
+
+  const dispatch = useDispatch();
+  const {
+    browserFields,
+    dataViewId,
+    loading: loadingSourcerer,
+    // important to get selectedPatterns from useSourcererDataView
+    // in order to include the exclude filters in the search that are not stored in the timeline
+    selectedPatterns,
+    sourcererDataView,
+  } = useSourcererDataView(SourcererScopeName.timeline);
+  /*
+   * `pageIndex` needs to be maintained for each table in each tab independently
+   * and consequently it cannot be the part of common redux state
+   * of the timeline.
+   *
+   */
+
+  const { uiSettings, timelineDataService } = useKibana().services;
+  const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]);
+  const kqlQuery: {
+    query: string;
+    language: KueryFilterQueryKind;
+  } = useMemo(
+    () => ({ query: kqlQueryExpression.trim(), language: kqlQueryLanguage }),
+    [kqlQueryExpression, kqlQueryLanguage]
+  );
+
+  const combinedQueries = useMemo(() => {
+    return combineQueries({
+      config: esQueryConfig,
+      dataProviders,
+      indexPattern: sourcererDataView,
+      browserFields,
+      filters,
+      kqlQuery,
+      kqlMode,
+    });
+  }, [esQueryConfig, dataProviders, sourcererDataView, browserFields, filters, kqlQuery, kqlMode]);
+
+  useInvalidFilterQuery({
+    id: timelineId,
+    filterQuery: combinedQueries?.filterQuery,
+    kqlError: combinedQueries?.kqlError,
+    query: kqlQuery,
+    startDate: start,
+    endDate: end,
+  });
+
+  const isBlankTimeline: boolean =
+    isEmpty(dataProviders) &&
+    isEmpty(filters) &&
+    isEmpty(kqlQuery.query) &&
+    combinedQueries?.filterQuery === undefined;
+
+  const canQueryTimeline = useMemo(
+    () =>
+      combinedQueries != null &&
+      loadingSourcerer != null &&
+      !loadingSourcerer &&
+      !isEmpty(start) &&
+      !isEmpty(end) &&
+      combinedQueries?.filterQuery !== undefined,
+    [combinedQueries, end, loadingSourcerer, start]
+  );
+
+  const timelineQuerySortField = useMemo(() => {
+    return sort.map(({ columnId, columnType, esTypes, sortDirection }) => ({
+      field: columnId,
+      direction: sortDirection as Direction,
+      esTypes: esTypes ?? [],
+      type: columnType,
+    }));
+  }, [sort]);
+
+  const { defaultColumns } = useTimelineColumns(columns);
+
+  const [dataLoadingState, { totalCount }] = useTimelineEvents({
+    dataViewId,
+    endDate: end,
+    fields: emptyFieldsList,
+    filterQuery: combinedQueries?.filterQuery,
+    id: timelineId,
+    indexNames: selectedPatterns,
+    language: kqlQuery.language,
+    limit: 0, // We only care about the totalCount here
+    runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings,
+    skip: !canQueryTimeline,
+    sort: timelineQuerySortField,
+    startDate: start,
+    timerangeKind,
+  });
+
+  useEffect(() => {
+    dispatch(
+      timelineActions.initializeTimelineSettings({
+        id: timelineId,
+        defaultColumns,
+      })
+    );
+  }, [dispatch, timelineId, defaultColumns]);
+
+  // NOTE: The timeline is blank after browser FORWARD navigation (after using back button to navigate to
+  // the previous page from the timeline), yet we still see total count. This is because the timeline
+  // is not getting refreshed when using browser navigation.
+  const showEventsCountBadge = !isBlankTimeline && totalCount >= 0;
+
+  // <Synchronisation of the timeline data service>
+  // Sync the timerange
+  const timelineFilters = useTimelineDataFilters(isActiveTimeline(timelineId));
+  useEffect(() => {
+    timelineDataService.query.timefilter.timefilter.setTime({
+      from: timelineFilters.from,
+      to: timelineFilters.to,
+    });
+  }, [timelineDataService.query.timefilter.timefilter, timelineFilters.from, timelineFilters.to]);
+
+  // Sync the base query
+  useEffect(() => {
+    timelineDataService.query.queryString.setQuery(
+      // We're using the base query of all combined queries here, to account for all
+      // of timeline's query dependencies (data providers, query etc.)
+      combinedQueries?.baseKqlQuery || { language: kqlQueryLanguage, query: '' }
+    );
+  }, [timelineDataService, combinedQueries, kqlQueryLanguage]);
+  // </Synchronisation of the timeline data service>
+
+  if (!showEventsCountBadge) return null;
+
+  return dataLoadingState === DataLoadingState.loading ||
+    dataLoadingState === DataLoadingState.loadingMore ? (
+    <EuiLoadingSpinner size="s" />
+  ) : (
+    <EventsCountBadge data-test-subj="query-events-count">{totalCount}</EventsCountBadge>
+  );
+};
+
+const TimelineQueryTabEventsCount = React.memo(TimelineQueryTabEventsCountComponent);
+
+// eslint-disable-next-line import/no-default-export
+export { TimelineQueryTabEventsCount as default };
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx
index 76a88afdb8058..277010473e6bd 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx
@@ -10,14 +10,16 @@ import React from 'react';
 import { TimelineDataTable } from '.';
 import { TimelineId, TimelineTabs } from '../../../../../../common/types';
 import { DataLoadingState } from '@kbn/unified-data-table';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { DataView } from '@kbn/data-views-plugin/common';
 import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
 import { useSourcererDataView } from '../../../../../sourcerer/containers';
 import type { ComponentProps } from 'react';
 import { getColumnHeaders } from '../../body/column_headers/helpers';
 import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks';
 import * as timelineActions from '../../../../store/actions';
-import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
 import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
+import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
 
 jest.mock('../../../../../sourcerer/containers');
 
@@ -54,6 +56,11 @@ type TestComponentProps = Partial<ComponentProps<typeof TimelineDataTable>> & {
 // that is why we are setting it to 10s
 const SPECIAL_TEST_TIMEOUT = 50000;
 
+const mockDataView = new DataView({
+  spec: mockSourcererScope.sourcererDataView,
+  fieldFormats: fieldFormatsMock,
+});
+
 const TestComponent = (props: TestComponentProps) => {
   const { store = createMockStore(), ...restProps } = props;
   useSourcererDataView();
@@ -62,6 +69,7 @@ const TestComponent = (props: TestComponentProps) => {
       <TimelineDataTable
         columns={initialEnrichedColumns}
         columnIds={initialEnrichedColumnsIds}
+        dataView={mockDataView}
         activeTab={TimelineTabs.query}
         timelineId={TimelineId.test}
         itemsPerPage={50}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx
index 35bed4ddd804b..f2ac443db9afd 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx
@@ -24,7 +24,6 @@ import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_det
 import { selectTimelineById } from '../../../../store/selectors';
 import { RowRendererCount } from '../../../../../../common/api/timeline';
 import { EmptyComponent } from '../../../../../common/lib/cell_actions/helpers';
-import { withDataView } from '../../../../../common/components/with_data_view';
 import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
 import type { TimelineItem } from '../../../../../../common/search_strategy';
 import { useKibana } from '../../../../../common/lib/kibana';
@@ -432,7 +431,7 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
   }
 );
 
-export const TimelineDataTable = withDataView<DataTableProps>(TimelineDataTableComponent);
+export const TimelineDataTable = React.memo(TimelineDataTableComponent);
 
 // eslint-disable-next-line import/no-default-export
 export { TimelineDataTable as default };
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx
index 872f3c760f38b..79acb78023fc8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx
@@ -34,6 +34,8 @@ import { DataLoadingState } from '@kbn/unified-data-table';
 import { getColumnHeaders } from '../body/column_headers/helpers';
 import { defaultUdtHeaders } from '../body/column_headers/default_headers';
 import type { ColumnHeaderType } from '../../../../../common/types';
+import { DataView } from '@kbn/data-views-plugin/common';
+import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
 
 jest.mock('../../../containers', () => ({
   useTimelineEvents: jest.fn(),
@@ -85,6 +87,11 @@ const SPECIAL_TEST_TIMEOUT = 50000;
 
 const localMockedTimelineData = structuredClone(mockTimelineData);
 
+const mockDataView = new DataView({
+  spec: mockSourcererScope.sourcererDataView,
+  fieldFormats: fieldFormatsMock,
+});
+
 const TestComponent = (
   props: Partial<ComponentProps<typeof UnifiedTimeline>> & { show?: boolean }
 ) => {
@@ -92,6 +99,7 @@ const TestComponent = (
   const testComponentDefaultProps: ComponentProps<typeof QueryTabContent> = {
     columns: getColumnHeaders(columnsToDisplay, mockSourcererScope.browserFields),
     activeTab: TimelineTabs.query,
+    dataView: mockDataView,
     rowRenderers: [],
     timelineId: TimelineId.test,
     itemsPerPage: 10,
@@ -513,50 +521,14 @@ describe('unified timeline', () => {
   });
 
   describe('unified field list', () => {
-    describe('render', () => {
-      let TestProviderWithNewStore: FC<PropsWithChildren<unknown>>;
-      beforeEach(() => {
-        const freshStore = createMockStore();
-        // eslint-disable-next-line react/display-name
-        TestProviderWithNewStore = ({ children }) => {
-          return <TestProviders store={freshStore}>{children}</TestProviders>;
-        };
-      });
-      it(
-        'should not render when timeline has never been opened',
-        async () => {
-          render(<TestComponent show={false} />, {
-            wrapper: TestProviderWithNewStore,
-          });
-          expect(await screen.queryByTestId('timeline-sidebar')).not.toBeInTheDocument();
-        },
-        SPECIAL_TEST_TIMEOUT
-      );
-
-      it(
-        'should render when timeline has been opened',
-        async () => {
-          render(<TestComponent />, {
-            wrapper: TestProviderWithNewStore,
-          });
-          expect(await screen.queryByTestId('timeline-sidebar')).toBeInTheDocument();
-        },
-        SPECIAL_TEST_TIMEOUT
-      );
-
-      it(
-        'should not re-render when timeline has been opened at least once',
-        async () => {
-          const { rerender } = render(<TestComponent />, {
-            wrapper: TestProviderWithNewStore,
-          });
-          rerender(<TestComponent show={false} />);
-          // Even after timeline is closed, it should still exist in the background
-          expect(await screen.queryByTestId('timeline-sidebar')).toBeInTheDocument();
-        },
-        SPECIAL_TEST_TIMEOUT
-      );
-    });
+    it(
+      'should render',
+      async () => {
+        renderTestComponents();
+        expect(await screen.queryByTestId('timeline-sidebar')).toBeInTheDocument();
+      },
+      SPECIAL_TEST_TIMEOUT
+    );
 
     it(
       'should be able to add filters',
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx
index 890cca680a67a..2b4a2e627649e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx
@@ -6,7 +6,7 @@
  */
 import type { EuiDataGridProps } from '@elastic/eui';
 import { EuiFlexGroup, EuiFlexItem, EuiHideFor, useEuiTheme } from '@elastic/eui';
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useMemo, useRef, useState } from 'react';
 import { useDispatch } from 'react-redux';
 import { generateFilters } from '@kbn/data-plugin/public';
 import type { DataView, DataViewField } from '@kbn/data-plugin/common';
@@ -26,8 +26,6 @@ import { UnifiedFieldListSidebarContainer } from '@kbn/unified-field-list';
 import type { EuiTheme } from '@kbn/react-kibana-context-styled';
 import type { CoreStart } from '@kbn/core-lifecycle-browser';
 import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-import { withDataView } from '../../../../common/components/with_data_view';
 import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
 import type { TimelineItem } from '../../../../../common/search_strategy';
 import { useKibana } from '../../../../common/lib/kibana';
@@ -47,7 +45,6 @@ import TimelineDataTable from './data_table';
 import { timelineActions } from '../../../store';
 import { getFieldsListCreationOptions } from './get_fields_list_creation_options';
 import { defaultUdtHeaders } from '../body/column_headers/default_headers';
-import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
 
 const TimelineBodyContainer = styled.div.attrs(({ className = '' }) => ({
   className: `${className}`,
@@ -347,31 +344,6 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
     onFieldEdited();
   }, [onFieldEdited]);
 
-  // PERFORMANCE ONLY CODE BLOCK
-  /**
-   * We check for the timeline open status to request the fields for the fields browser as the fields request
-   * is often a much longer running request for customers with a significant number of indices and fields in those indices.
-   * This request should only be made after the user has decided to interact with timeline to prevent any performance impacts
-   * to the underlying security solution views, as this query will always run when the timeline exists on the page.
-   *
-   * `hasTimelineBeenOpenedOnce` - We want to keep timeline loading times as fast as possible after the user
-   * has chosen to interact with timeline at least once, so we use this flag to prevent re-requesting of this fields data
-   * every time timeline is closed and re-opened after the first interaction.
-   */
-
-  const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
-  const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId));
-
-  const [hasTimelineBeenOpenedOnce, setHasTimelineBeenOpenedOnce] = useState(false);
-
-  useEffect(() => {
-    if (!hasTimelineBeenOpenedOnce && show) {
-      setHasTimelineBeenOpenedOnce(true);
-    }
-  }, [hasTimelineBeenOpenedOnce, show]);
-
-  // END PERFORMANCE ONLY CODE BLOCK
-
   return (
     <TimelineBodyContainer className="timelineBodyContainer" ref={setSidebarContainer}>
       <TimelineResizableLayout
@@ -380,7 +352,7 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
         sidebarPanel={
           <SidebarPanelFlexGroup gutterSize="none">
             <EuiFlexItem className="sidebarContainer">
-              {dataView && hasTimelineBeenOpenedOnce ? (
+              {dataView && (
                 <UnifiedFieldListSidebarContainer
                   ref={unifiedFieldListContainerRef}
                   showFieldList
@@ -396,7 +368,7 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
                   onAddFilter={onAddFilter}
                   onFieldEdited={wrappedOnFieldEdited}
                 />
-              ) : null}
+              )}
             </EuiFlexItem>
             <EuiHideFor sizes={HIDE_FOR_SIZES}>
               <EuiFlexItem
@@ -430,6 +402,7 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
                     <DataGridMemoized
                       columns={columns}
                       columnIds={currentColumnIds}
+                      dataView={dataView}
                       rowRenderers={rowRenderers}
                       timelineId={timelineId}
                       isSortEnabled={isSortEnabled}
@@ -463,6 +436,6 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
   );
 };
 
-export const UnifiedTimeline = React.memo(withDataView<Props>(UnifiedTimelineComponent));
+export const UnifiedTimeline = React.memo(UnifiedTimelineComponent);
 // eslint-disable-next-line import/no-default-export
 export { UnifiedTimeline as default };
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx
index 7b261981c4062..5ede370e9aabb 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx
@@ -219,22 +219,27 @@ export const useTimelineEventsHandler = ({
     setActiveBatch(0);
   }, [limit]);
 
-  const [timelineResponse, setTimelineResponse] = useState<TimelineArgs>({
-    id,
-    inspect: {
-      dsl: [],
-      response: [],
-    },
-    refetch: () => {},
-    totalCount: -1,
-    pageInfo: {
-      activePage: 0,
-      querySize: 0,
-    },
-    events: [],
-    loadNextBatch,
-    refreshedAt: 0,
-  });
+  const defaultTimelineResponse = useMemo(
+    () => ({
+      id,
+      inspect: {
+        dsl: [],
+        response: [],
+      },
+      refetch: () => {},
+      totalCount: -1,
+      pageInfo: {
+        activePage: 0,
+        querySize: 0,
+      },
+      events: [],
+      loadNextBatch,
+      refreshedAt: 0,
+    }),
+    [id, loadNextBatch]
+  );
+
+  const [timelineResponse, setTimelineResponse] = useState<TimelineArgs>(defaultTimelineResponse);
 
   const timelineSearch = useCallback(
     async (
@@ -375,95 +380,98 @@ export const useTimelineEventsHandler = ({
       return;
     }
 
-    setTimelineRequest((prevRequest) => {
-      const prevEqlRequest = prevRequest as TimelineEqlRequestOptionsInput;
-      const prevSearchParameters = {
-        defaultIndex: prevRequest?.defaultIndex ?? [],
-        filterQuery: prevRequest?.filterQuery ?? '',
-        sort: prevRequest?.sort ?? initSortDefault,
-        timerange: prevRequest?.timerange ?? {},
-        runtimeMappings: (prevRequest?.runtimeMappings ?? {}) as unknown as RunTimeMappings,
-        ...deStructureEqlOptions(prevEqlRequest),
-      };
-
-      const timerange =
-        startDate && endDate
-          ? { timerange: { interval: '12h', from: startDate, to: endDate } }
-          : {};
-      const currentSearchParameters = {
-        defaultIndex: indexNames,
-        filterQuery: createFilter(filterQuery),
-        sort,
-        runtimeMappings: runtimeMappings ?? {},
-        ...timerange,
-        ...deStructureEqlOptions(eqlOptions),
-      };
-
-      const areSearchParamsSame = deepEqual(prevSearchParameters, currentSearchParameters);
-
-      const newActiveBatch = !areSearchParamsSame ? 0 : activeBatch;
+    // Only set timeline request when an actual query exists
+    if (filterQuery || eqlOptions?.query) {
+      setTimelineRequest((prevRequest) => {
+        const prevEqlRequest = prevRequest as TimelineEqlRequestOptionsInput;
+        const prevSearchParameters = {
+          defaultIndex: prevRequest?.defaultIndex ?? [],
+          filterQuery: prevRequest?.filterQuery ?? '',
+          sort: prevRequest?.sort ?? initSortDefault,
+          timerange: prevRequest?.timerange ?? {},
+          runtimeMappings: (prevRequest?.runtimeMappings ?? {}) as unknown as RunTimeMappings,
+          ...deStructureEqlOptions(prevEqlRequest),
+        };
 
-      /*
-       * optimization to avoid unnecessary network request when a field
-       * has already been fetched
-       *
-       */
+        const timerange =
+          startDate && endDate
+            ? { timerange: { interval: '12h', from: startDate, to: endDate } }
+            : {};
+        const currentSearchParameters = {
+          defaultIndex: indexNames,
+          filterQuery: createFilter(filterQuery),
+          sort,
+          runtimeMappings: runtimeMappings ?? {},
+          ...timerange,
+          ...deStructureEqlOptions(eqlOptions),
+        };
 
-      let finalFieldRequest = fields;
+        const areSearchParamsSame = deepEqual(prevSearchParameters, currentSearchParameters);
 
-      const newFieldsRequested = fields.filter(
-        (field) => !prevRequest?.fieldRequested?.includes(field)
-      );
-      if (newFieldsRequested.length > 0) {
-        finalFieldRequest = [...(prevRequest?.fieldRequested ?? []), ...newFieldsRequested];
-      } else {
-        finalFieldRequest = prevRequest?.fieldRequested ?? [];
-      }
+        const newActiveBatch = !areSearchParamsSame ? 0 : activeBatch;
 
-      let newPagination = {
         /*
+         * optimization to avoid unnecessary network request when a field
+         * has already been fetched
          *
-         * fetches data cumulatively for the batches upto the activeBatch
-         * This is needed because, we want to get incremental data as well for the old batches
-         * For example, newly requested fields
-         *
-         * */
-        activePage: newActiveBatch,
-        querySize: limit,
-      };
+         */
+
+        let finalFieldRequest = fields;
+
+        const newFieldsRequested = fields.filter(
+          (field) => !prevRequest?.fieldRequested?.includes(field)
+        );
+        if (newFieldsRequested.length > 0) {
+          finalFieldRequest = [...(prevRequest?.fieldRequested ?? []), ...newFieldsRequested];
+        } else {
+          finalFieldRequest = prevRequest?.fieldRequested ?? [];
+        }
 
-      if (newFieldsRequested.length > 0) {
-        newPagination = {
-          activePage: 0,
-          querySize: (newActiveBatch + 1) * limit,
+        let newPagination = {
+          /*
+           *
+           * fetches data cumulatively for the batches upto the activeBatch
+           * This is needed because, we want to get incremental data as well for the old batches
+           * For example, newly requested fields
+           *
+           * */
+          activePage: newActiveBatch,
+          querySize: limit,
         };
-      }
 
-      const currentRequest = {
-        defaultIndex: indexNames,
-        factoryQueryType: TimelineEventsQueries.all,
-        fieldRequested: finalFieldRequest,
-        fields: finalFieldRequest,
-        filterQuery: createFilter(filterQuery),
-        pagination: newPagination,
-        language,
-        runtimeMappings,
-        sort,
-        ...timerange,
-        ...(eqlOptions ? eqlOptions : {}),
-      } as const;
-
-      if (activeBatch !== newActiveBatch) {
-        setActiveBatch(newActiveBatch);
-        if (id === TimelineId.active) {
-          activeTimeline.setActivePage(newActiveBatch);
+        if (newFieldsRequested.length > 0) {
+          newPagination = {
+            activePage: 0,
+            querySize: (newActiveBatch + 1) * limit,
+          };
         }
-      }
-      if (!deepEqual(prevRequest, currentRequest)) {
-        return currentRequest;
-      }
-      return prevRequest;
-    });
+
+        const currentRequest = {
+          defaultIndex: indexNames,
+          factoryQueryType: TimelineEventsQueries.all,
+          fieldRequested: finalFieldRequest,
+          fields: finalFieldRequest,
+          filterQuery: createFilter(filterQuery),
+          pagination: newPagination,
+          language,
+          runtimeMappings,
+          sort,
+          ...timerange,
+          ...(eqlOptions ? eqlOptions : {}),
+        } as const;
+
+        if (activeBatch !== newActiveBatch) {
+          setActiveBatch(newActiveBatch);
+          if (id === TimelineId.active) {
+            activeTimeline.setActivePage(newActiveBatch);
+          }
+        }
+        if (!deepEqual(prevRequest, currentRequest)) {
+          return currentRequest;
+        }
+        return prevRequest;
+      });
+    }
   }, [
     dispatch,
     indexNames,
@@ -486,24 +494,9 @@ export const useTimelineEventsHandler = ({
   */
   useEffect(() => {
     if (isEmpty(filterQuery)) {
-      setTimelineResponse({
-        id,
-        inspect: {
-          dsl: [],
-          response: [],
-        },
-        refetch: () => {},
-        totalCount: -1,
-        pageInfo: {
-          activePage: 0,
-          querySize: 0,
-        },
-        events: [],
-        loadNextBatch,
-        refreshedAt: 0,
-      });
+      setTimelineResponse(defaultTimelineResponse);
     }
-  }, [filterQuery, id, loadNextBatch]);
+  }, [defaultTimelineResponse, filterQuery]);
 
   const timelineSearchHandler = useCallback(
     async (onNextHandler?: OnNextResponseHandler) => {
@@ -529,6 +522,8 @@ export const useTimelineEventsHandler = ({
   return [loading, finalTimelineLineResponse, timelineSearchHandler];
 };
 
+const defaultEvents: TimelineItem[][] = [];
+
 export const useTimelineEvents = ({
   dataViewId,
   endDate,
@@ -545,7 +540,7 @@ export const useTimelineEvents = ({
   skip = false,
   timerangeKind,
 }: UseTimelineEventsProps): [DataLoadingState, TimelineArgs] => {
-  const [eventsPerPage, setEventsPerPage] = useState<TimelineItem[][]>([[]]);
+  const [eventsPerPage, setEventsPerPage] = useState<TimelineItem[][]>(defaultEvents);
   const [dataLoadingState, timelineResponse, timelineSearchHandler] = useTimelineEventsHandler({
     dataViewId,
     endDate,
@@ -576,9 +571,15 @@ export const useTimelineEvents = ({
     const { activePage, querySize } = timelineResponse.pageInfo;
 
     setEventsPerPage((prev) => {
-      let result = [...prev];
+      let result = structuredClone(prev);
+      const newEventsLength = timelineResponse.events.length;
+      const oldEventsLength = result.length;
+
       if (querySize === limit && activePage > 0) {
         result[activePage] = timelineResponse.events;
+      } else if (oldEventsLength === 0 && newEventsLength === 0) {
+        // don't change array reference if no actual changes take place
+        result = prev;
       } else {
         result = [timelineResponse.events];
       }


Reviews:
user: login: logeekal
body: Done code review as of now. Will post the results of Desk Testing soon.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:00:40Z
user: login: michaelolo24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T15:49:14Z
user: login: michaelolo24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:03:08Z
user: login: michaelolo24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:04:10Z
user: login: michaelolo24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:11:31Z
user: login: michaelolo24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:27:44Z
user: login: michaelolo24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:41:15Z
user: login: logeekal
body: Desk Testing looks good. Below are couple of observations:

## Timeline Search Strategy firing twice

When there is a timeline bottom bar, `timelineSearchStrategy` is firing twice to get the count of events, everytime. See demo below:

https://github.com/user-attachments/assets/32381315-1b35-4395-b859-0ba5d9ee87b5


## App Perf with 500K total fields

I tried app performance with 500K total fields with : 
- one document having all 500K fields populated
- around 10K docs with 25K fields populated

the whole app feels slow specially because `useInitSourcerer` takes around 10s to finish and get all the fields when visiting Alerts Page. a

Below URL  took 12s to resolve and that essential blocks whole Alerts page. And app also seems slow. Not sure why.

<img width="493" alt="image" src="https://github.com/user-attachments/assets/ae22a4f6-55e5-4676-abd4-d63035644710" />

`http://localhost:5601/internal/data_views/fields?pattern=.alerts-security.alerts-default%2Capm-*-transaction*%2Cauditbeat-*%2Cendgame-*%2Cfilebeat-*%2Clogs-*%2Cpacketbeat-*%2Ctest*%2Ctraces-apm*%2Cwinlogbeat-*%2C-*elastic-cloud-logs-*&meta_fields=_source&meta_fields=_id&meta_fields=_index&meta_fields=_score&meta_fields=_ignored&allow_no_index=true&apiVersion=1`



https://github.com/user-attachments/assets/404c13cf-c146-4232-8d0e-fc5862a0a42d




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:49:37Z
user: login: logeekal
body: >Can you retest, I was unable to replicate the duplicate queries...and the gap is from the borealis changes as that is also currently on main, but not 8.18

@michaelolo24 , I retested and can confirm that now only single query is firing. I also noticed that `gap` is only in `light` theme and not in borealis 🤦 . 

Thanks for making these changes.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:17:46Z

Review comments:
diff_hunk: @@ -486,24 +494,9 @@ export const useTimelineEventsHandler = ({
   */
   useEffect(() => {
     if (isEmpty(filterQuery)) {
-      setTimelineResponse({
-        id,
-        inspect: {
-          dsl: [],
-          response: [],
-        },
-        refetch: () => {},
-        totalCount: -1,
-        pageInfo: {
-          activePage: 0,
-          querySize: 0,
-        },
-        events: [],
-        loadNextBatch,
-        refreshedAt: 0,
-      });
+      setTimelineResponse(defaultTimelineResponse);
path: x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx
user: login: logeekal
body: ✨ 
created_at: 2025-02-27T17:34:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -529,6 +522,8 @@ export const useTimelineEventsHandler = ({
   return [loading, finalTimelineLineResponse, timelineSearchHandler];
 };
 
+const defaultEvents: TimelineItem[][] = [];
path: x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx
user: login: logeekal
body: might result in error somewhere since it is a 2D array and previously `eventsPerPage[0]` was `[]` and with this change it is `undefined`
created_at: 2025-02-27T17:36:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
+import type { TimelineId } from '../../../../../common/types';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
+
+/**
+ * We check for the timeline open status to request the fields for the fields browser as the fields request
+ * is often a much longer running request for customers with a significant number of indices and fields in those indices.
+ * This request should only be made after the user has decided to interact with a specific tab in the timeline to prevent any performance impacts
+ * to the underlying security solution views, as this query will always run when the timeline exists on the page.
+ *
+ * `hasTimelineTabBeenOpenedOnce` - We want to keep timeline loading times as fast as possible after the user
+ * has chosen to interact with timeline at least once, so we use this flag to prevent re-requesting of this fields data
+ * every time timeline is closed and re-opened after the first interaction.
+ */
+
+export interface OnDemandRendererProps {
+  children: React.ReactElement | null;
+  dataTestSubj: string;
+  isOverflowYScroll?: boolean;
+  shouldShowTab: boolean;
+  timelineId: TimelineId;
+}
+
+export const OnDemandRenderer = React.memo(
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.tsx
user: login: logeekal
body: Since it is specific to Timeline's tab, i would suggest renaming it something like `LazyTimelineTabRenderer`.
created_at: 2025-03-06T12:32:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
+import type { TimelineId } from '../../../../../common/types';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
+
+/**
+ * We check for the timeline open status to request the fields for the fields browser as the fields request
+ * is often a much longer running request for customers with a significant number of indices and fields in those indices.
+ * This request should only be made after the user has decided to interact with a specific tab in the timeline to prevent any performance impacts
+ * to the underlying security solution views, as this query will always run when the timeline exists on the page.
+ *
+ * `hasTimelineTabBeenOpenedOnce` - We want to keep timeline loading times as fast as possible after the user
+ * has chosen to interact with timeline at least once, so we use this flag to prevent re-requesting of this fields data
+ * every time timeline is closed and re-opened after the first interaction.
+ */
+
+export interface OnDemandRendererProps {
+  children: React.ReactElement | null;
+  dataTestSubj: string;
+  isOverflowYScroll?: boolean;
+  shouldShowTab: boolean;
+  timelineId: TimelineId;
+}
+
+export const OnDemandRenderer = React.memo(
+  ({
+    children,
+    dataTestSubj,
+    shouldShowTab,
+    isOverflowYScroll,
+    timelineId,
+  }: OnDemandRendererProps) => {
+    const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
+    const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId));
+
+    const [hasTimelineTabBeenOpenedOnce, setHasTimelineTabBeenOpenedOnce] = useState(false);
+
+    useEffect(() => {
+      if (!hasTimelineTabBeenOpenedOnce && show && shouldShowTab) {
+        setHasTimelineTabBeenOpenedOnce(true);
+      }
+    }, [hasTimelineTabBeenOpenedOnce, shouldShowTab, show]);
+
+    return (
+      <div
+        // The shouldShowTab check here is necessary for the flex container to accurately size to the modal window when it's opened
+        css={css`
+          display: ${shouldShowTab ? 'flex' : 'none'};
+          overflow: ${isOverflowYScroll ? 'hidden scroll' : 'hidden'};
+          flex: 1;
+        `}
+        data-test-subj={dataTestSubj}
+      >
+        {hasTimelineTabBeenOpenedOnce ? children : <EuiLoadingElastic size="xl" />}
+      </div>
+    );
+  }
+);
+
+OnDemandRenderer.displayName = 'OnDemandRenderer';
+
+export const OnDemandSuspenseFallback = () => (
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.tsx
user: login: logeekal
body: Since this component is not in itself related to `React.suspense`, I would suggest renaming it to `TimelineTabFallback`
created_at: 2025-03-06T12:33:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
+import type { TimelineId } from '../../../../../common/types';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
+
+/**
+ * We check for the timeline open status to request the fields for the fields browser as the fields request
+ * is often a much longer running request for customers with a significant number of indices and fields in those indices.
+ * This request should only be made after the user has decided to interact with a specific tab in the timeline to prevent any performance impacts
+ * to the underlying security solution views, as this query will always run when the timeline exists on the page.
+ *
+ * `hasTimelineTabBeenOpenedOnce` - We want to keep timeline loading times as fast as possible after the user
+ * has chosen to interact with timeline at least once, so we use this flag to prevent re-requesting of this fields data
+ * every time timeline is closed and re-opened after the first interaction.
+ */
+
+export interface OnDemandRendererProps {
+  children: React.ReactElement | null;
+  dataTestSubj: string;
+  isOverflowYScroll?: boolean;
+  shouldShowTab: boolean;
+  timelineId: TimelineId;
+}
+
+export const OnDemandRenderer = React.memo(
+  ({
+    children,
+    dataTestSubj,
+    shouldShowTab,
+    isOverflowYScroll,
+    timelineId,
+  }: OnDemandRendererProps) => {
+    const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
+    const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId));
+
+    const [hasTimelineTabBeenOpenedOnce, setHasTimelineTabBeenOpenedOnce] = useState(false);
+
+    useEffect(() => {
+      if (!hasTimelineTabBeenOpenedOnce && show && shouldShowTab) {
+        setHasTimelineTabBeenOpenedOnce(true);
+      }
+    }, [hasTimelineTabBeenOpenedOnce, shouldShowTab, show]);
+
+    return (
+      <div
+        // The shouldShowTab check here is necessary for the flex container to accurately size to the modal window when it's opened
+        css={css`
+          display: ${shouldShowTab ? 'flex' : 'none'};
+          overflow: ${isOverflowYScroll ? 'hidden scroll' : 'hidden'};
+          flex: 1;
+        `}
+        data-test-subj={dataTestSubj}
+      >
+        {hasTimelineTabBeenOpenedOnce ? children : <EuiLoadingElastic size="xl" />}
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.tsx
user: login: logeekal
body: `nit` : what do you think about below change?
```suggestion
        {hasTimelineTabBeenOpenedOnce ? children : <OnDemandSuspenseFallback />}
```
created_at: 2025-03-06T12:37:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
+import type { TimelineId } from '../../../../../common/types';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
+
+/**
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.tsx
user: login: logeekal
body: Thanks for this TSDoc, i think it is worth moving to the line 33 so that it shows up in the description of `OnDemandRenderer`
created_at: 2025-03-06T12:39:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect } from 'react';
+import { render } from '@testing-library/react';
+import type { OnDemandRendererProps } from './on_demand_renderer';
+import { OnDemandRenderer } from './on_demand_renderer';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { TimelineId } from '../../../../../common/types';
+
+jest.mock('../../../../common/hooks/use_selector');
+
+describe('OnDemandRenderer', () => {
+  const mockUseDeepEqualSelector = useDeepEqualSelector as jest.Mock;
+  const defaultProps = {
+    dataTestSubj: 'test',
+    shouldShowTab: true,
+    isOverflowYScroll: false,
+    timelineId: TimelineId.test,
+  };
+
+  const TestComponent = ({ children, ...restProps }: Partial<OnDemandRendererProps>) => (
+    <OnDemandRenderer {...defaultProps} {...restProps}>
+      <div>{children ?? 'test component'}</div>
+    </OnDemandRenderer>
+  );
+  const renderTestComponents = (props?: Partial<OnDemandRendererProps>) => {
+    const { children, ...restProps } = props ?? {};
+    return render(<TestComponent {...restProps}>{children}</TestComponent>);
+  };
+
+  beforeEach(() => {
+    mockUseDeepEqualSelector.mockClear();
+  });
+
+  describe('timeline visibility', () => {
+    it('should NOT render children when the timeline show status is false', () => {
+      mockUseDeepEqualSelector.mockReturnValue({ show: false });
+      const { queryByText } = renderTestComponents();
+      expect(queryByText('test component')).not.toBeInTheDocument();
+    });
+
+    it('should render children when the timeline show status is true', () => {
+      mockUseDeepEqualSelector.mockReturnValue({ show: true });
+
+      const { getByText } = renderTestComponents();
+
+      expect(getByText('test component')).toBeInTheDocument();
+    });
+  });
+
+  describe('tab visibility', () => {
+    it('should not render children when show tab is false', () => {
+      const { queryByText } = renderTestComponents({ shouldShowTab: false });
+
+      expect(queryByText('test component')).not.toBeInTheDocument();
+    });
+  });
+
+  describe('re-rendering', () => {
+    const testChildString = 'new content';
+    const mockFnShouldThatShouldOnlyRunOnce = jest.fn();
+
+    const TestChild = () => {
+      useEffect(() => {
+        mockFnShouldThatShouldOnlyRunOnce();
+      }, []);
+      return <div>{testChildString}</div>;
+    };
+
+    const RerenderTestComponent = (props?: Partial<OnDemandRendererProps>) => (
+      <TestComponent {...props}>
+        <TestChild />
+      </TestComponent>
+    );
+
+    beforeEach(() => {
+      jest.resetAllMocks();
+      mockUseDeepEqualSelector.mockReturnValue({ show: true });
+    });
+
+    it('should NOT re-render children after the first render', () => {
+      const { rerender, queryByText } = render(<RerenderTestComponent />);
+      rerender(<RerenderTestComponent />);
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.test.tsx
user: login: logeekal
body: Since `rerender` is forwarded with same props as `render`, the `RenderTestComponent` will not be updated and `useEffect` will not run.

I think this test will always pass irrespective of the logic of `OnDemandRender`. My first thought was what if you pass `shouldShowTab` as `false` now in `rerender` but I see that you have already covered that in other tests.

Not sure if in this test  `rerender` command is even needed.
created_at: 2025-03-06T12:45:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isEmpty } from 'lodash/fp';
+import React, { useEffect, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
+import { getEsQueryConfig } from '@kbn/data-plugin/common';
+import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
+import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
+import { useTimelineDataFilters } from '../../../../containers/use_timeline_data_filters';
+import { useInvalidFilterQuery } from '../../../../../common/hooks/use_invalid_filter_query';
+import { timelineActions, timelineSelectors } from '../../../../store';
+import type { Direction } from '../../../../../../common/search_strategy';
+import { useTimelineEvents } from '../../../../containers';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { combineQueries } from '../../../../../common/lib/kuery';
+import type {
+  KueryFilterQuery,
+  KueryFilterQueryKind,
+} from '../../../../../../common/types/timeline';
+import type { inputsModel } from '../../../../../common/store';
+import { inputsSelectors } from '../../../../../common/store';
+import { SourcererScopeName } from '../../../../../sourcerer/store/model';
+import { timelineDefaults } from '../../../../store/defaults';
+import { useSourcererDataView } from '../../../../../sourcerer/containers';
+import { isActiveTimeline } from '../../../../../helpers';
+import type { TimelineModel } from '../../../../store/model';
+import { useTimelineColumns } from '../shared/use_timeline_columns';
+import { EventsCountBadge } from '../shared/layout';
+
+/**
+ * TODO: This component is a pared down duplicate of the logic used in timeline/tabs/query/index.tsx
+ * This is only done to support the events count badge that shows in the bottom bar of the application,
+ * without needing to render the entire query tab, which is expensive to render at a significant enough fields count.
+ * The long term solution is a centralized query either via RTK or useQuery, that both can read from, but that is out of scope
+ * at this current time.
+ */
+
+export const TimelineQueryTabEventsCountComponent: React.FC<{ timelineId: string }> = ({
+  timelineId,
+}) => {
+  const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+  const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterKuerySelector(), []);
+  const getInputsTimeline = useMemo(() => inputsSelectors.getTimelineSelector(), []);
+
+  const timeline: TimelineModel = useDeepEqualSelector(
+    (state) => getTimeline(state, timelineId) ?? timelineDefaults
+  );
+  const input: inputsModel.InputsRange = useDeepEqualSelector((state) => getInputsTimeline(state));
+  const { timerange: { to: end, from: start, kind: timerangeKind } = {} } = input;
+  const {
+    columns,
+    dataProviders,
+    filters: currentTimelineFilters,
+    kqlMode,
+    sort,
+    timelineType,
+  } = timeline;
+
+  const kqlQueryTimeline: KueryFilterQuery | null = useDeepEqualSelector((state) =>
+    getKqlQueryTimeline(state, timelineId)
+  );
+  const filters = useMemo(
+    () => (kqlMode === 'filter' ? currentTimelineFilters || [] : []),
+    [currentTimelineFilters, kqlMode]
+  );
+
+  // return events on empty search
+  const kqlQueryExpression =
+    isEmpty(dataProviders) &&
+    isEmpty(kqlQueryTimeline?.expression ?? '') &&
+    timelineType === 'template'
+      ? ' '
+      : kqlQueryTimeline?.expression ?? '';
+
+  const kqlQueryLanguage =
+    isEmpty(dataProviders) && timelineType === 'template'
+      ? 'kuery'
+      : kqlQueryTimeline?.kind ?? 'kuery';
+
+  const dispatch = useDispatch();
+  const {
+    browserFields,
+    dataViewId,
+    loading: loadingSourcerer,
+    // important to get selectedPatterns from useSourcererDataView
+    // in order to include the exclude filters in the search that are not stored in the timeline
+    selectedPatterns,
+    sourcererDataView,
+  } = useSourcererDataView(SourcererScopeName.timeline);
+  /*
+   * `pageIndex` needs to be maintained for each table in each tab independently
+   * and consequently it cannot be the part of common redux state
+   * of the timeline.
+   *
+   */
+
+  const { uiSettings, timelineDataService } = useKibana().services;
+  const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]);
+  const kqlQuery: {
+    query: string;
+    language: KueryFilterQueryKind;
+  } = useMemo(
+    () => ({ query: kqlQueryExpression.trim(), language: kqlQueryLanguage }),
+    [kqlQueryExpression, kqlQueryLanguage]
+  );
+
+  const combinedQueries = useMemo(() => {
+    return combineQueries({
+      config: esQueryConfig,
+      dataProviders,
+      indexPattern: sourcererDataView,
+      browserFields,
+      filters,
+      kqlQuery,
+      kqlMode,
+    });
+  }, [esQueryConfig, dataProviders, sourcererDataView, browserFields, filters, kqlQuery, kqlMode]);
+
+  useInvalidFilterQuery({
+    id: timelineId,
+    filterQuery: combinedQueries?.filterQuery,
+    kqlError: combinedQueries?.kqlError,
+    query: kqlQuery,
+    startDate: start,
+    endDate: end,
+  });
+
+  const isBlankTimeline: boolean =
+    isEmpty(dataProviders) &&
+    isEmpty(filters) &&
+    isEmpty(kqlQuery.query) &&
+    combinedQueries?.filterQuery === undefined;
+
+  const canQueryTimeline = useMemo(
+    () =>
+      combinedQueries != null &&
+      loadingSourcerer != null &&
+      !loadingSourcerer &&
+      !isEmpty(start) &&
+      !isEmpty(end) &&
+      combinedQueries?.filterQuery !== undefined,
+    [combinedQueries, end, loadingSourcerer, start]
+  );
+
+  const timelineQuerySortField = useMemo(() => {
+    return sort.map(({ columnId, columnType, esTypes, sortDirection }) => ({
+      field: columnId,
+      direction: sortDirection as Direction,
+      esTypes: esTypes ?? [],
+      type: columnType,
+    }));
+  }, [sort]);
+
+  const { defaultColumns } = useTimelineColumns(columns);
+
+  const [_, { totalCount }] = useTimelineEvents({
+    dataViewId,
+    endDate: end,
+    fields: [],
+    filterQuery: combinedQueries?.filterQuery,
+    id: timelineId,
+    indexNames: selectedPatterns,
+    language: kqlQuery.language,
+    limit: 0,
+    runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings,
+    skip: !canQueryTimeline,
+    sort: timelineQuerySortField,
+    startDate: start,
+    timerangeKind,
+  });
+
+  useEffect(() => {
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/events_count.tsx
user: login: logeekal
body: Do you think we need to remove similar `useEffect` in query and eql tab so there it does not override the effect of this `useEffect` 🤷 
created_at: 2025-03-06T12:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,7 +65,7 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(
           </EuiFlexItem>
           {!show && ( // this is a hack because TimelineEventsCountBadge is using react-reverse-portal so the component which is used in multiple places cannot be visible in multiple places at the same time
             <EuiFlexItem grow={false} data-test-subj="timeline-event-count-badge">
-              <TimelineEventsCountBadge />
+              <TimelineQueryTabEventsCount timelineId={timelineId} />
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx
user: login: logeekal
body: Are you planning to remove that `OutPortal` `InPortal` stuff as well in this PR?
created_at: 2025-03-06T13:00:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -529,6 +522,8 @@ export const useTimelineEventsHandler = ({
   return [loading, finalTimelineLineResponse, timelineSearchHandler];
 };
 
+const defaultEvents: TimelineItem[][] = [];
path: x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx
user: login: michaelolo24
body: It's only accessed on line 597, which would only be doing empty work to flatten a nested empty array. Happy to revert it, but not sure if it's necessary
created_at: 2025-03-06T15:49:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
+import type { TimelineId } from '../../../../../common/types';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
+
+/**
+ * We check for the timeline open status to request the fields for the fields browser as the fields request
+ * is often a much longer running request for customers with a significant number of indices and fields in those indices.
+ * This request should only be made after the user has decided to interact with a specific tab in the timeline to prevent any performance impacts
+ * to the underlying security solution views, as this query will always run when the timeline exists on the page.
+ *
+ * `hasTimelineTabBeenOpenedOnce` - We want to keep timeline loading times as fast as possible after the user
+ * has chosen to interact with timeline at least once, so we use this flag to prevent re-requesting of this fields data
+ * every time timeline is closed and re-opened after the first interaction.
+ */
+
+export interface OnDemandRendererProps {
+  children: React.ReactElement | null;
+  dataTestSubj: string;
+  isOverflowYScroll?: boolean;
+  shouldShowTab: boolean;
+  timelineId: TimelineId;
+}
+
+export const OnDemandRenderer = React.memo(
+  ({
+    children,
+    dataTestSubj,
+    shouldShowTab,
+    isOverflowYScroll,
+    timelineId,
+  }: OnDemandRendererProps) => {
+    const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
+    const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId));
+
+    const [hasTimelineTabBeenOpenedOnce, setHasTimelineTabBeenOpenedOnce] = useState(false);
+
+    useEffect(() => {
+      if (!hasTimelineTabBeenOpenedOnce && show && shouldShowTab) {
+        setHasTimelineTabBeenOpenedOnce(true);
+      }
+    }, [hasTimelineTabBeenOpenedOnce, shouldShowTab, show]);
+
+    return (
+      <div
+        // The shouldShowTab check here is necessary for the flex container to accurately size to the modal window when it's opened
+        css={css`
+          display: ${shouldShowTab ? 'flex' : 'none'};
+          overflow: ${isOverflowYScroll ? 'hidden scroll' : 'hidden'};
+          flex: 1;
+        `}
+        data-test-subj={dataTestSubj}
+      >
+        {hasTimelineTabBeenOpenedOnce ? children : <EuiLoadingElastic size="xl" />}
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.tsx
user: login: michaelolo24
body: Love it
created_at: 2025-03-06T16:03:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
+import type { TimelineId } from '../../../../../common/types';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
+
+/**
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.tsx
user: login: michaelolo24
body: Done, nice catch. Thanks!
created_at: 2025-03-06T16:04:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect } from 'react';
+import { render } from '@testing-library/react';
+import type { OnDemandRendererProps } from './on_demand_renderer';
+import { OnDemandRenderer } from './on_demand_renderer';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { TimelineId } from '../../../../../common/types';
+
+jest.mock('../../../../common/hooks/use_selector');
+
+describe('OnDemandRenderer', () => {
+  const mockUseDeepEqualSelector = useDeepEqualSelector as jest.Mock;
+  const defaultProps = {
+    dataTestSubj: 'test',
+    shouldShowTab: true,
+    isOverflowYScroll: false,
+    timelineId: TimelineId.test,
+  };
+
+  const TestComponent = ({ children, ...restProps }: Partial<OnDemandRendererProps>) => (
+    <OnDemandRenderer {...defaultProps} {...restProps}>
+      <div>{children ?? 'test component'}</div>
+    </OnDemandRenderer>
+  );
+  const renderTestComponents = (props?: Partial<OnDemandRendererProps>) => {
+    const { children, ...restProps } = props ?? {};
+    return render(<TestComponent {...restProps}>{children}</TestComponent>);
+  };
+
+  beforeEach(() => {
+    mockUseDeepEqualSelector.mockClear();
+  });
+
+  describe('timeline visibility', () => {
+    it('should NOT render children when the timeline show status is false', () => {
+      mockUseDeepEqualSelector.mockReturnValue({ show: false });
+      const { queryByText } = renderTestComponents();
+      expect(queryByText('test component')).not.toBeInTheDocument();
+    });
+
+    it('should render children when the timeline show status is true', () => {
+      mockUseDeepEqualSelector.mockReturnValue({ show: true });
+
+      const { getByText } = renderTestComponents();
+
+      expect(getByText('test component')).toBeInTheDocument();
+    });
+  });
+
+  describe('tab visibility', () => {
+    it('should not render children when show tab is false', () => {
+      const { queryByText } = renderTestComponents({ shouldShowTab: false });
+
+      expect(queryByText('test component')).not.toBeInTheDocument();
+    });
+  });
+
+  describe('re-rendering', () => {
+    const testChildString = 'new content';
+    const mockFnShouldThatShouldOnlyRunOnce = jest.fn();
+
+    const TestChild = () => {
+      useEffect(() => {
+        mockFnShouldThatShouldOnlyRunOnce();
+      }, []);
+      return <div>{testChildString}</div>;
+    };
+
+    const RerenderTestComponent = (props?: Partial<OnDemandRendererProps>) => (
+      <TestComponent {...props}>
+        <TestChild />
+      </TestComponent>
+    );
+
+    beforeEach(() => {
+      jest.resetAllMocks();
+      mockUseDeepEqualSelector.mockReturnValue({ show: true });
+    });
+
+    it('should NOT re-render children after the first render', () => {
+      const { rerender, queryByText } = render(<RerenderTestComponent />);
+      rerender(<RerenderTestComponent />);
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/on_demand_renderer.test.tsx
user: login: michaelolo24
body: Yep, nice catch. Thanks!
created_at: 2025-03-06T16:11:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isEmpty } from 'lodash/fp';
+import React, { useEffect, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
+import { getEsQueryConfig } from '@kbn/data-plugin/common';
+import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
+import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
+import { useTimelineDataFilters } from '../../../../containers/use_timeline_data_filters';
+import { useInvalidFilterQuery } from '../../../../../common/hooks/use_invalid_filter_query';
+import { timelineActions, timelineSelectors } from '../../../../store';
+import type { Direction } from '../../../../../../common/search_strategy';
+import { useTimelineEvents } from '../../../../containers';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { combineQueries } from '../../../../../common/lib/kuery';
+import type {
+  KueryFilterQuery,
+  KueryFilterQueryKind,
+} from '../../../../../../common/types/timeline';
+import type { inputsModel } from '../../../../../common/store';
+import { inputsSelectors } from '../../../../../common/store';
+import { SourcererScopeName } from '../../../../../sourcerer/store/model';
+import { timelineDefaults } from '../../../../store/defaults';
+import { useSourcererDataView } from '../../../../../sourcerer/containers';
+import { isActiveTimeline } from '../../../../../helpers';
+import type { TimelineModel } from '../../../../store/model';
+import { useTimelineColumns } from '../shared/use_timeline_columns';
+import { EventsCountBadge } from '../shared/layout';
+
+/**
+ * TODO: This component is a pared down duplicate of the logic used in timeline/tabs/query/index.tsx
+ * This is only done to support the events count badge that shows in the bottom bar of the application,
+ * without needing to render the entire query tab, which is expensive to render at a significant enough fields count.
+ * The long term solution is a centralized query either via RTK or useQuery, that both can read from, but that is out of scope
+ * at this current time.
+ */
+
+export const TimelineQueryTabEventsCountComponent: React.FC<{ timelineId: string }> = ({
+  timelineId,
+}) => {
+  const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+  const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterKuerySelector(), []);
+  const getInputsTimeline = useMemo(() => inputsSelectors.getTimelineSelector(), []);
+
+  const timeline: TimelineModel = useDeepEqualSelector(
+    (state) => getTimeline(state, timelineId) ?? timelineDefaults
+  );
+  const input: inputsModel.InputsRange = useDeepEqualSelector((state) => getInputsTimeline(state));
+  const { timerange: { to: end, from: start, kind: timerangeKind } = {} } = input;
+  const {
+    columns,
+    dataProviders,
+    filters: currentTimelineFilters,
+    kqlMode,
+    sort,
+    timelineType,
+  } = timeline;
+
+  const kqlQueryTimeline: KueryFilterQuery | null = useDeepEqualSelector((state) =>
+    getKqlQueryTimeline(state, timelineId)
+  );
+  const filters = useMemo(
+    () => (kqlMode === 'filter' ? currentTimelineFilters || [] : []),
+    [currentTimelineFilters, kqlMode]
+  );
+
+  // return events on empty search
+  const kqlQueryExpression =
+    isEmpty(dataProviders) &&
+    isEmpty(kqlQueryTimeline?.expression ?? '') &&
+    timelineType === 'template'
+      ? ' '
+      : kqlQueryTimeline?.expression ?? '';
+
+  const kqlQueryLanguage =
+    isEmpty(dataProviders) && timelineType === 'template'
+      ? 'kuery'
+      : kqlQueryTimeline?.kind ?? 'kuery';
+
+  const dispatch = useDispatch();
+  const {
+    browserFields,
+    dataViewId,
+    loading: loadingSourcerer,
+    // important to get selectedPatterns from useSourcererDataView
+    // in order to include the exclude filters in the search that are not stored in the timeline
+    selectedPatterns,
+    sourcererDataView,
+  } = useSourcererDataView(SourcererScopeName.timeline);
+  /*
+   * `pageIndex` needs to be maintained for each table in each tab independently
+   * and consequently it cannot be the part of common redux state
+   * of the timeline.
+   *
+   */
+
+  const { uiSettings, timelineDataService } = useKibana().services;
+  const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]);
+  const kqlQuery: {
+    query: string;
+    language: KueryFilterQueryKind;
+  } = useMemo(
+    () => ({ query: kqlQueryExpression.trim(), language: kqlQueryLanguage }),
+    [kqlQueryExpression, kqlQueryLanguage]
+  );
+
+  const combinedQueries = useMemo(() => {
+    return combineQueries({
+      config: esQueryConfig,
+      dataProviders,
+      indexPattern: sourcererDataView,
+      browserFields,
+      filters,
+      kqlQuery,
+      kqlMode,
+    });
+  }, [esQueryConfig, dataProviders, sourcererDataView, browserFields, filters, kqlQuery, kqlMode]);
+
+  useInvalidFilterQuery({
+    id: timelineId,
+    filterQuery: combinedQueries?.filterQuery,
+    kqlError: combinedQueries?.kqlError,
+    query: kqlQuery,
+    startDate: start,
+    endDate: end,
+  });
+
+  const isBlankTimeline: boolean =
+    isEmpty(dataProviders) &&
+    isEmpty(filters) &&
+    isEmpty(kqlQuery.query) &&
+    combinedQueries?.filterQuery === undefined;
+
+  const canQueryTimeline = useMemo(
+    () =>
+      combinedQueries != null &&
+      loadingSourcerer != null &&
+      !loadingSourcerer &&
+      !isEmpty(start) &&
+      !isEmpty(end) &&
+      combinedQueries?.filterQuery !== undefined,
+    [combinedQueries, end, loadingSourcerer, start]
+  );
+
+  const timelineQuerySortField = useMemo(() => {
+    return sort.map(({ columnId, columnType, esTypes, sortDirection }) => ({
+      field: columnId,
+      direction: sortDirection as Direction,
+      esTypes: esTypes ?? [],
+      type: columnType,
+    }));
+  }, [sort]);
+
+  const { defaultColumns } = useTimelineColumns(columns);
+
+  const [_, { totalCount }] = useTimelineEvents({
+    dataViewId,
+    endDate: end,
+    fields: [],
+    filterQuery: combinedQueries?.filterQuery,
+    id: timelineId,
+    indexNames: selectedPatterns,
+    language: kqlQuery.language,
+    limit: 0,
+    runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings,
+    skip: !canQueryTimeline,
+    sort: timelineQuerySortField,
+    startDate: start,
+    timerangeKind,
+  });
+
+  useEffect(() => {
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/events_count.tsx
user: login: michaelolo24
body: Potentially, but it should be aligned with the query tab as they should be running the same query string. I don't expect this change to be necessary
created_at: 2025-03-06T16:27:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,7 +65,7 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(
           </EuiFlexItem>
           {!show && ( // this is a hack because TimelineEventsCountBadge is using react-reverse-portal so the component which is used in multiple places cannot be visible in multiple places at the same time
             <EuiFlexItem grow={false} data-test-subj="timeline-event-count-badge">
-              <TimelineEventsCountBadge />
+              <TimelineQueryTabEventsCount timelineId={timelineId} />
path: x-pack/solutions/security/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx
user: login: michaelolo24
body: No, I wasn't planning on making that change in this PR 
created_at: 2025-03-06T16:41:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticmachine
created_at: 2025-02-26T16:17:55Z
body: Pinging @elastic/security-threat-hunting-investigations (Team:Threat Hunting:Investigations)
author_association: CONTRIBUTOR
user: login: logeekal
created_at: 2025-03-07T12:46:44Z
body: One more feedback. it looks like this PR introduces a wierd space in timeline as highlighted below. for comparison, i have posted a screenshot from [Release-sec](https://release-sec.kb.us-west2.gcp.elastic-cloud.com/app/security/alerts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!(.alerts-security.alerts-default)))&timeline=(activeTab:query,graphEventId:%27%27,isOpen:!t)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272025-03-06T23:00:00.000Z%27,fromStr:now%2Fd,kind:absolute,to:%272025-03-07T22:59:59.999Z%27,toStr:now%2Fd)),timeline:(linkTo:!(global),timerange:(from:%272025-03-06T23:00:00.000Z%27,fromStr:now%2Fd,kind:absolute,to:%272025-03-07T22:59:59.999Z%27,toStr:now%2Fd)))&pageFilters=!((exclude:!f,existsSelected:!f,fieldName:kibana.alert.workflow_status,hideActionBar:!t,selectedOptions:!(open),title:Status),(exclude:!f,existsSelected:!f,fieldName:kibana.alert.severity,hideActionBar:!t,selectedOptions:!(),title:Severity),(exclude:!f,existsSelected:!f,fieldName:user.name,hideActionBar:!f,selectedOptions:!(),title:User),(exclude:!f,existsSelected:!f,fieldName:host.name,hideActionBar:!f,selectedOptions:!(),title:Host))) instance.

<img width="1726" alt="Screenshot 2025-03-07 at 13 42 58" src="https://github.com/user-attachments/assets/7d494f76-f69a-4af3-874b-12b30d512a9f" />

<img width="1022" alt="Screenshot 2025-03-07 at 13 45 54" src="https://github.com/user-attachments/assets/a00ae203-1517-4c7b-be22-8cb0868b36f8" />


author_association: CONTRIBUTOR
user: login: michaelolo24
created_at: 2025-03-07T20:23:59Z
body: @logeekal - Can you retest, I was unable to replicate the duplicate queries...and the gap is from the borealis changes as that is also currently on main, but not 8.18
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-03-10T14:07:00Z
body: ## :yellow_heart: Build succeeded, but was flaky
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/282199)
* Commit: c81c4a50e1c3f233008c2f3cefb6ff5bfe410333
* [Cloud Deployment](https://kibana-pr-212478.kb.us-west2.gcp.elastic-cloud.com)

### Failed CI Steps
* [Detection Engine - Security Solution Cypress Tests #5](https://buildkite.com/elastic/kibana-pull-request/builds/282199#0195802d-16a2-4876-8a2e-7d6516936ca9)

### Test Failures
* [[job]](https://buildkite.com/elastic/kibana-pull-request/builds/282199#0195802d-16a2-4876-8a2e-7d6516936ca9) [[logs]](https://buildkite.com/organizations/elastic/pipelines/kibana-pull-request/builds/282199/jobs/0195802d-16a2-4876-8a2e-7d6516936ca9/artifacts/0195806b-3f86-4f9e-bba1-d0097a179803) Detection Engine - Security Solution Cypress Tests #<span></span>5 / Custom query rules Custom detection rules creation with data views Creates and enables a new rule Creates and enables a new rule

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
#### Module Count
> Fewer modules leads to a faster build time


| id | [before](https://github.com/elastic/kibana/commit/49ebf9e43e7848666fe720afd4365385aee7628e) | [after](https://github.com/elastic/kibana/commit/c81c4a50e1c3f233008c2f3cefb6ff5bfe410333) | diff |
| --- | --- | --- | --- |
| `securitySolution` | 7062 | 7064 | +2 |

#### Async chunks
> Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app


| id | [before](https://github.com/elastic/kibana/commit/49ebf9e43e7848666fe720afd4365385aee7628e) | [after](https://github.com/elastic/kibana/commit/c81c4a50e1c3f233008c2f3cefb6ff5bfe410333) | diff |
| --- | --- | --- | --- |
| `securitySolution` | 8.9MB | 8.8MB | -13.2KB |

#### Page load bundle
> Size of the bundles that are downloaded on every page load. Target size is below 100kb


| id | [before](https://github.com/elastic/kibana/commit/49ebf9e43e7848666fe720afd4365385aee7628e) | [after](https://github.com/elastic/kibana/commit/c81c4a50e1c3f233008c2f3cefb6ff5bfe410333) | diff |
| --- | --- | --- | --- |
| `securitySolution` | 87.5KB | 87.5KB | -9.0B |



### History
* :yellow_heart: [Build #281813](https://buildkite.com/elastic/kibana-pull-request/builds/281813) was flaky d1c290b1d285f514e1ac244268420b3bf16602ef
* :green_heart: [Build #281587](https://buildkite.com/elastic/kibana-pull-request/builds/281587) succeeded e3c8e54bd0c0b1cead00bd56e5e4129916864ab8
* :broken_heart: [Build #281555](https://buildkite.com/elastic/kibana-pull-request/builds/281555) failed 7536190dd72c3809274348e835919415b42379ca
* :broken_heart: [Build #281399](https://buildkite.com/elastic/kibana-pull-request/builds/281399) failed c3585261c3e9fb16a9c345d7c245f0f6c23a0a39

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/282199","number":282199,"commit":"c81c4a50e1c3f233008c2f3cefb6ff5bfe410333"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281813","number":281813,"commit":"d1c290b1d285f514e1ac244268420b3bf16602ef"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281587","number":281587,"commit":"e3c8e54bd0c0b1cead00bd56e5e4129916864ab8"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281555","number":281555,"commit":"7536190dd72c3809274348e835919415b42379ca"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281399","number":281399,"commit":"c3585261c3e9fb16a9c345d7c245f0f6c23a0a39"}],"number":282199}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-11T16:57:03Z
body: Starting backport for target branches: 8.17, 8.x

https://github.com/elastic/kibana/actions/runs/13793767412
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-11T17:09:59Z
body: ## 💔 Some backports could not be created

| Status | Branch | Result |
|:------:|:------:|:------|
|❌|8.17|Backport failed because of merge conflicts|
|✅|8.x|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/213978">](https://github.com/elastic/kibana/pull/213978)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Manual backport
To create the backport manually run:
```
node scripts/backport --pr 212478
```

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR
user: login: michaelolo24
created_at: 2025-03-11T18:23:19Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.17|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/213988">](https://github.com/elastic/kibana/pull/213988)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-13T17:48:45Z
body: Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-14T17:48:52Z
body: Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-17T17:49:03Z
body: Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-18T18:48:47Z
body: Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-19T19:48:56Z
body: Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-20T20:49:04Z
body: Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-21T21:48:59Z
body: Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
author_association: CONTRIBUTOR
user: login: michaelolo24
created_at: 2025-03-24T18:50:11Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.x|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/215791">](https://github.com/elastic/kibana/pull/215791)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR
user: login: mistic
created_at: 2025-03-25T16:50:38Z
body: This PR didn't land on time for the latest 8.17.4 BC. Updating the labels.
author_association: CONTRIBUTOR
user: login: michaelolo24
created_at: 2025-04-07T16:11:32Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|9.0|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/217362">](https://github.com/elastic/kibana/pull/217362)|
|✅|8.18|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/217364">](https://github.com/elastic/kibana/pull/217364)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR
user: login: michaelolo24
created_at: 2025-06-06T13:09:29Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|9.0|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/222986">](https://github.com/elastic/kibana/pull/222986)|
|✅|8.18|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/222987">](https://github.com/elastic/kibana/pull/222987)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 10.0.0 -->
author_association: CONTRIBUTOR
user: login: michaelolo24
created_at: 2025-06-06T13:14:40Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|9.0|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/222989">](https://github.com/elastic/kibana/pull/222989)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 10.0.0 -->
author_association: CONTRIBUTOR